From 6dc61a430ba3a8480399309f277e5debfd6403ba Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Mon, 15 May 2023 00:38:27 -0500 Subject: [PATCH 001/508] Sort embedded collections in Nfo files Because the Nfo files emit the collections as they are in-memory, the files are not stable in format, genres, tags, albums, people, etc. are emitted in random orders. Add ordering of the collections when emitting the Nfo files so the file remains stable (unchanged) when underlying media information doesn't change. In the process of this, it became clear that most of the providers and probes don't trim the strings like people's names, genre names, etc. so did a pass of Trim cleanup too. Specific ordering: (alphabetical/numeric ascending after trimming blanks and defaulting to zero for missing numbers) BaseItem: Directors, Writers, Trailers (by Url), Production Locations, Genres, Studios, Tags, Custom Provider Data (by key), Linked Children (by Path>LibraryItemId), Backdrop Images (by path), Actors (by SortOrder>Name) AlbumNfo: Artists, Album Artists, Tracks (by ParentIndexNumber>IndexNumber>Name) ArtistNfo: Albums (by Production Year>SortName>Name) MovieNfo: Artists Fix Debug build lint Fix CI debug build lint issue. Fix review issues Fixed debug-build lint issues. Emits the `disc` number to NFO for tracks with a non-zero ParentIndexNumber and only emit `position` if non-zero. Removed the exception filtering I put in for testing. Don't emit actors for MusicAlbums or MusicArtists Swap from String.Trimmed() to ?.Trim() Addressing PR feedback Can't use ReadOnlySpan in an async method Removed now-unused namespace --- MediaBrowser.Controller/Entities/BaseItem.cs | 6 ++-- .../Entities/PeopleHelper.cs | 2 ++ .../Sorting/SortExtensions.cs | 5 +++ .../Parsers/BaseItemXmlParser.cs | 27 ++++++++------ .../Probing/ProbeResultNormalizer.cs | 34 ++++++++++-------- .../MediaInfo/AudioFileProber.cs | 21 ++++++----- .../MediaInfo/FFProbeVideoInfo.cs | 7 ++-- .../Music/AlbumMetadataService.cs | 4 +-- .../Plugins/Omdb/OmdbProvider.cs | 4 +-- .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 7 ++-- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 6 ++-- .../Plugins/Tmdb/TV/TmdbSeasonProvider.cs | 9 ++--- .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 4 +-- .../Savers/AlbumNfoSaver.cs | 17 ++++++--- .../Savers/ArtistNfoSaver.cs | 7 +++- .../Savers/BaseNfoSaver.cs | 35 ++++++++++++------- .../Savers/MovieNfoSaver.cs | 3 +- 17 files changed, 123 insertions(+), 75 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 414488853f..8201ae318b 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -22,6 +22,7 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -915,7 +916,7 @@ namespace MediaBrowser.Controller.Entities // Remove from middle if surrounded by spaces sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal); - // Remove from end if followed by a space + // Remove from end if preceeded by a space if (sortable.EndsWith(" " + search, StringComparison.Ordinal)) { sortable = sortable.Remove(sortable.Length - (search.Length + 1)); @@ -1769,7 +1770,6 @@ namespace MediaBrowser.Controller.Entities public void AddStudio(string name) { ArgumentException.ThrowIfNullOrEmpty(name); - var current = Studios; if (!current.Contains(name, StringComparison.OrdinalIgnoreCase)) @@ -1788,7 +1788,7 @@ namespace MediaBrowser.Controller.Entities public void SetStudios(IEnumerable names) { - Studios = names.Distinct().ToArray(); + Studios = names.Trimmed().Distinct().ToArray(); } /// diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs index 5292bd7727..d818604365 100644 --- a/MediaBrowser.Controller/Entities/PeopleHelper.cs +++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs @@ -15,6 +15,8 @@ namespace MediaBrowser.Controller.Entities ArgumentNullException.ThrowIfNull(person); ArgumentException.ThrowIfNullOrEmpty(person.Name); + person.Name = person.Name.Trim(); + // Normalize if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs index f9c0d39ddd..db934e0f47 100644 --- a/MediaBrowser.Controller/Sorting/SortExtensions.cs +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -30,5 +30,10 @@ namespace MediaBrowser.Controller.Sorting { return list.ThenByDescending(getName, _comparer); } + + public static IEnumerable Trimmed(this IEnumerable values) + { + return values.Select(i => (i ?? string.Empty).Trim()); + } } } diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index e4ac59b676..119effe791 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -234,8 +234,8 @@ namespace MediaBrowser.LocalMetadata.Parsers item.CustomRating = reader.ReadNormalizedString(); break; case "RunningTime": - var runtimeText = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(runtimeText)) + var runtimeText = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(runtimeText)) { if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) { @@ -253,7 +253,7 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "LockData": - item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); + item.IsLocked = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase); break; case "Network": foreach (var name in reader.GetStringArray()) @@ -331,9 +331,9 @@ namespace MediaBrowser.LocalMetadata.Parsers case "Rating": case "IMDBrating": { - var rating = reader.ReadElementContentAsString(); + var rating = reader.ReadNormalizedString(); - if (!string.IsNullOrWhiteSpace(rating)) + if (!string.IsNullOrEmpty(rating)) { // All external meta is saving this as '.' for decimal I believe...but just to be sure if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val)) @@ -449,7 +449,7 @@ namespace MediaBrowser.LocalMetadata.Parsers case "OwnerUserId": { - var val = reader.ReadElementContentAsString(); + var val = reader.ReadNormalizedString(); if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty)) { @@ -464,7 +464,7 @@ namespace MediaBrowser.LocalMetadata.Parsers case "Format3D": { - var val = reader.ReadElementContentAsString(); + var val = reader.ReadNormalizedString(); if (item is Video video) { @@ -498,7 +498,7 @@ namespace MediaBrowser.LocalMetadata.Parsers string readerName = reader.Name; if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue)) { - var id = reader.ReadElementContentAsString(); + var id = reader.ReadNormalizedString(); item.TrySetProviderId(providerIdValue, id); } else @@ -580,7 +580,12 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Tagline": - item.Tagline = reader.ReadNormalizedString(); + var val = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(val)) + { + item.Tagline = val; + } + break; default: reader.Skip(); @@ -842,7 +847,7 @@ namespace MediaBrowser.LocalMetadata.Parsers userId = reader.ReadNormalizedString(); break; case "CanEdit": - canEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); + canEdit = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase); break; default: reader.Skip(); @@ -856,7 +861,7 @@ namespace MediaBrowser.LocalMetadata.Parsers } // This is valid - if (!string.IsNullOrWhiteSpace(userId) && Guid.TryParse(userId, out var guid)) + if (!string.IsNullOrEmpty(userId) && Guid.TryParse(userId, out var guid)) { return new PlaylistUserPermissions(guid, canEdit); } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 334796f585..0dee77db81 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -10,7 +10,9 @@ using System.Text.RegularExpressions; using System.Xml; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -531,42 +533,44 @@ namespace MediaBrowser.MediaEncoding.Probing private void ProcessPairs(string key, List pairs, MediaInfo info) { List peoples = new List(); + var distinctPairs = pairs.Select(p => p.Value) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Trimmed() + .Distinct(StringComparer.OrdinalIgnoreCase); + if (string.Equals(key, "studio", StringComparison.OrdinalIgnoreCase)) { - info.Studios = pairs.Select(p => p.Value) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); + info.Studios = distinctPairs.ToArray(); } else if (string.Equals(key, "screenwriters", StringComparison.OrdinalIgnoreCase)) { - foreach (var pair in pairs) + foreach (var pair in distinctPairs) { peoples.Add(new BaseItemPerson { - Name = pair.Value, + Name = pair, Type = PersonKind.Writer }); } } else if (string.Equals(key, "producers", StringComparison.OrdinalIgnoreCase)) { - foreach (var pair in pairs) + foreach (var pair in distinctPairs) { peoples.Add(new BaseItemPerson { - Name = pair.Value, + Name = pair, Type = PersonKind.Producer }); } } else if (string.Equals(key, "directors", StringComparison.OrdinalIgnoreCase)) { - foreach (var pair in pairs) + foreach (var pair in distinctPairs) { peoples.Add(new BaseItemPerson { - Name = pair.Value, + Name = pair, Type = PersonKind.Director }); } @@ -591,10 +595,10 @@ namespace MediaBrowser.MediaEncoding.Probing switch (reader.Name) { case "key": - name = reader.ReadElementContentAsString(); + name = reader.ReadNormalizedString(); break; case "string": - value = reader.ReadElementContentAsString(); + value = reader.ReadNormalizedString(); break; default: reader.Skip(); @@ -607,8 +611,8 @@ namespace MediaBrowser.MediaEncoding.Probing } } - if (string.IsNullOrWhiteSpace(name) - || string.IsNullOrWhiteSpace(value)) + if (string.IsNullOrEmpty(name) + || string.IsNullOrEmpty(value)) { return null; } @@ -1453,7 +1457,7 @@ namespace MediaBrowser.MediaEncoding.Probing var genres = new List(info.Genres); foreach (var genre in Split(genreVal, true)) { - if (string.IsNullOrWhiteSpace(genre)) + if (string.IsNullOrEmpty(genre)) { continue; } diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 80bb1a514c..d113cabc7f 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -13,6 +13,7 @@ using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -183,11 +184,11 @@ namespace MediaBrowser.Providers.MediaInfo foreach (var albumArtist in albumArtists) { - if (!string.IsNullOrEmpty(albumArtist)) + if (!string.IsNullOrWhiteSpace(albumArtist)) { PeopleHelper.AddPerson(people, new PersonInfo { - Name = albumArtist, + Name = albumArtist.Trim(), Type = PersonKind.AlbumArtist }); } @@ -215,11 +216,11 @@ namespace MediaBrowser.Providers.MediaInfo foreach (var performer in performers) { - if (!string.IsNullOrEmpty(performer)) + if (!string.IsNullOrWhiteSpace(performer)) { PeopleHelper.AddPerson(people, new PersonInfo { - Name = performer, + Name = performer.Trim(), Type = PersonKind.Artist }); } @@ -227,11 +228,11 @@ namespace MediaBrowser.Providers.MediaInfo foreach (var composer in track.Composer.Split(InternalValueSeparator)) { - if (!string.IsNullOrEmpty(composer)) + if (!string.IsNullOrWhiteSpace(composer)) { PeopleHelper.AddPerson(people, new PersonInfo { - Name = composer, + Name = composer.Trim(), Type = PersonKind.Composer }); } @@ -273,13 +274,13 @@ namespace MediaBrowser.Providers.MediaInfo if (options.ReplaceAllMetadata) { - audio.Album = track.Album; + audio.Album = track.Album.Trim(); audio.IndexNumber = track.TrackNumber; audio.ParentIndexNumber = track.DiscNumber; } else { - audio.Album ??= track.Album; + audio.Album ??= track.Album.Trim(); audio.IndexNumber ??= track.TrackNumber; audio.ParentIndexNumber ??= track.DiscNumber; } @@ -309,13 +310,15 @@ namespace MediaBrowser.Providers.MediaInfo if (!audio.LockedFields.Contains(MetadataField.Genres)) { - var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator); if (libraryOptions.UseCustomTagDelimiters) { genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray(); } + genres = genres.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0 ? genres : audio.Genres; diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 246ba2733f..f486f150da 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -16,6 +16,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; @@ -400,7 +401,7 @@ namespace MediaBrowser.Providers.MediaInfo { video.Genres = Array.Empty(); - foreach (var genre in data.Genres) + foreach (var genre in data.Genres.Trimmed()) { video.AddGenre(genre); } @@ -509,9 +510,9 @@ namespace MediaBrowser.Providers.MediaInfo { PeopleHelper.AddPerson(people, new PersonInfo { - Name = person.Name, + Name = person.Name.Trim(), Type = person.Type, - Role = person.Role + Role = person.Role.Trim() }); } diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index a39bd16cea..daebe85d69 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -187,7 +187,7 @@ namespace MediaBrowser.Providers.Music { PeopleHelper.AddPerson(people, new PersonInfo { - Name = albumArtist, + Name = albumArtist.Trim(), Type = PersonKind.AlbumArtist }); } @@ -196,7 +196,7 @@ namespace MediaBrowser.Providers.Music { PeopleHelper.AddPerson(people, new PersonInfo { - Name = artist, + Name = artist.Trim(), Type = PersonKind.Artist }); } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index de0da7f7bd..ad9edb031c 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -421,7 +421,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb { var person = new PersonInfo { - Name = result.Director, + Name = result.Director.Trim(), Type = PersonKind.Director }; @@ -432,7 +432,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb { var person = new PersonInfo { - Name = result.Writer, + Name = result.Writer.Trim(), Type = PersonKind.Writer }; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 8d68e2dcfe..582e05b793 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -12,6 +12,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using TMDbLib.Objects.Find; @@ -234,7 +235,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var genres = movieResult.Genres; - foreach (var genre in genres.Select(g => g.Name)) + foreach (var genre in genres.Select(g => g.Name).Trimmed()) { movie.AddGenre(genre); } @@ -254,7 +255,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var personInfo = new PersonInfo { Name = actor.Name.Trim(), - Role = actor.Character, + Role = actor.Character.Trim(), Type = PersonKind.Actor, SortOrder = actor.Order }; @@ -289,7 +290,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var personInfo = new PersonInfo { Name = person.Name.Trim(), - Role = person.Job, + Role = person.Job?.Trim(), Type = type }; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index e628abde55..4ee1645531 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -211,7 +211,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV metadataResult.AddPerson(new PersonInfo { Name = actor.Name.Trim(), - Role = actor.Character, + Role = actor.Character.Trim(), Type = PersonKind.Actor, SortOrder = actor.Order }); @@ -225,7 +225,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV metadataResult.AddPerson(new PersonInfo { Name = guest.Name.Trim(), - Role = guest.Character, + Role = guest.Character.Trim(), Type = PersonKind.GuestStar, SortOrder = guest.Order }); @@ -249,7 +249,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV metadataResult.AddPerson(new PersonInfo { Name = person.Name.Trim(), - Role = person.Job, + Role = person.Job?.Trim(), Type = type }); } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index 3f208b5993..b0a1e00df9 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -82,12 +82,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var cast = credits.Cast.OrderBy(c => c.Order).Take(Plugin.Instance.Configuration.MaxCastMembers).ToList(); for (var i = 0; i < cast.Count; i++) { + var member = cast[i]; result.AddPerson(new PersonInfo { - Name = cast[i].Name.Trim(), - Role = cast[i].Character, + Name = member.Name.Trim(), + Role = member.Character.Trim(), Type = PersonKind.Actor, - SortOrder = cast[i].Order + SortOrder = member.Order }); } } @@ -108,7 +109,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV result.AddPerson(new PersonInfo { Name = person.Name.Trim(), - Role = person.Job, + Role = person.Job?.Trim(), Type = type }); } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index e4062740fe..9ace9c6743 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -330,7 +330,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var personInfo = new PersonInfo { Name = actor.Name.Trim(), - Role = actor.Character, + Role = actor.Character.Trim(), Type = PersonKind.Actor, SortOrder = actor.Order, ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath) @@ -368,7 +368,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV yield return new PersonInfo { Name = person.Name.Trim(), - Role = person.Job, + Role = person.Job?.Trim(), Type = type }; } diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs index 2385e70485..4cb6f81b73 100644 --- a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -55,12 +56,12 @@ namespace MediaBrowser.XbmcMetadata.Savers { var album = (MusicAlbum)item; - foreach (var artist in album.Artists) + foreach (var artist in album.Artists.Trimmed().OrderBy(artist => artist)) { writer.WriteElementString("artist", artist); } - foreach (var artist in album.AlbumArtists) + foreach (var artist in album.AlbumArtists.Trimmed().OrderBy(artist => artist)) { writer.WriteElementString("albumartist", artist); } @@ -70,11 +71,19 @@ namespace MediaBrowser.XbmcMetadata.Savers private void AddTracks(IEnumerable tracks, XmlWriter writer) { - foreach (var track in tracks.OrderBy(i => i.ParentIndexNumber ?? 0).ThenBy(i => i.IndexNumber ?? 0)) + foreach (var track in tracks + .OrderBy(i => i.ParentIndexNumber ?? 0) + .ThenBy(i => i.IndexNumber ?? 0) + .ThenBy(i => i.Name?.Trim())) { writer.WriteStartElement("track"); - if (track.IndexNumber.HasValue) + if (track.ParentIndexNumber.HasValue && track.ParentIndexNumber.Value != 0) + { + writer.WriteElementString("disc", track.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (track.IndexNumber.HasValue && track.IndexNumber.Value != 0) { writer.WriteElementString("position", track.IndexNumber.Value.ToString(CultureInfo.InvariantCulture)); } diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs index 813d75f6c1..e13ba9385f 100644 --- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Xml; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.IO; using MediaBrowser.XbmcMetadata.Configuration; using Microsoft.Extensions.Logging; @@ -69,7 +71,10 @@ namespace MediaBrowser.XbmcMetadata.Savers private void AddAlbums(IList albums, XmlWriter writer) { - foreach (var album in albums) + foreach (var album in albums + .OrderBy(album => album.ProductionYear ?? 0) + .ThenBy(album => album.SortName?.Trim()) + .ThenBy(album => album.Name?.Trim())) { writer.WriteStartElement("album"); diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 2afec3f6cd..7c94b25c4c 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -19,6 +19,7 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -488,7 +489,9 @@ namespace MediaBrowser.XbmcMetadata.Savers var directors = people .Where(i => i.IsType(PersonKind.Director)) - .Select(i => i.Name) + .Select(i => i.Name?.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(i => i) .ToList(); foreach (var person in directors) @@ -498,8 +501,9 @@ namespace MediaBrowser.XbmcMetadata.Savers var writers = people .Where(i => i.IsType(PersonKind.Writer)) - .Select(i => i.Name) + .Select(i => i.Name?.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(i => i) .ToList(); foreach (var person in writers) @@ -512,7 +516,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("credits", person); } - foreach (var trailer in item.RemoteTrailers) + foreach (var trailer in item.RemoteTrailers.OrderBy(t => t.Url?.Trim())) { writer.WriteElementString("trailer", GetOutputTrailerUrl(trailer.Url)); } @@ -660,22 +664,22 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("tagline", item.Tagline); } - foreach (var country in item.ProductionLocations) + foreach (var country in item.ProductionLocations.Trimmed().OrderBy(country => country)) { writer.WriteElementString("country", country); } - foreach (var genre in item.Genres) + foreach (var genre in item.Genres.Trimmed().OrderBy(genre => genre)) { writer.WriteElementString("genre", genre); } - foreach (var studio in item.Studios) + foreach (var studio in item.Studios.Trimmed().OrderBy(studio => studio)) { writer.WriteElementString("studio", studio); } - foreach (var tag in item.Tags) + foreach (var tag in item.Tags.Trimmed().OrderBy(tag => tag)) { if (item is MusicAlbum || item is MusicArtist) { @@ -752,7 +756,7 @@ namespace MediaBrowser.XbmcMetadata.Savers if (item.ProviderIds is not null) { - foreach (var providerKey in item.ProviderIds.Keys) + foreach (var providerKey in item.ProviderIds.Keys.OrderBy(providerKey => providerKey)) { var providerId = item.ProviderIds[providerKey]; if (!string.IsNullOrEmpty(providerId) && !writtenProviderIds.Contains(providerKey)) @@ -764,7 +768,7 @@ namespace MediaBrowser.XbmcMetadata.Savers XmlConvert.VerifyName(tagName); Logger.LogDebug("Saving custom provider tagname {0}", tagName); - writer.WriteElementString(GetTagForProviderKey(providerKey), providerId); + writer.WriteElementString(tagName, providerId); } catch (ArgumentException) { @@ -785,7 +789,10 @@ namespace MediaBrowser.XbmcMetadata.Savers AddUserData(item, writer, userManager, userDataRepo, options); - AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo); + if (item is not MusicAlbum && item is not MusicArtist) + { + AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo); + } if (item is BoxSet folder) { @@ -797,6 +804,8 @@ namespace MediaBrowser.XbmcMetadata.Savers { var items = item.LinkedChildren .Where(i => i.Type == LinkedChildType.Manual) + .OrderBy(i => i.Path?.Trim()) + .ThenBy(i => i.LibraryItemId?.Trim()) .ToList(); foreach (var link in items) @@ -839,7 +848,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("poster", GetImagePathToSave(image, libraryManager)); } - foreach (var backdrop in item.GetImages(ImageType.Backdrop)) + foreach (var backdrop in item.GetImages(ImageType.Backdrop).OrderBy(b => b.Path?.Trim())) { writer.WriteElementString("fanart", GetImagePathToSave(backdrop, libraryManager)); } @@ -913,7 +922,9 @@ namespace MediaBrowser.XbmcMetadata.Savers private void AddActors(List people, XmlWriter writer, ILibraryManager libraryManager, bool saveImagePath) { - foreach (var person in people) + foreach (var person in people + .OrderBy(person => person.SortOrder ?? 0) + .ThenBy(person => person.Name?.Trim())) { if (person.IsType(PersonKind.Director) || person.IsType(PersonKind.Writer)) { diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index bc344d87e0..3ff8749e74 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -100,7 +101,7 @@ namespace MediaBrowser.XbmcMetadata.Savers if (item is MusicVideo musicVideo) { - foreach (var artist in musicVideo.Artists) + foreach (var artist in musicVideo.Artists.Trimmed().OrderBy(artist => artist)) { writer.WriteElementString("artist", artist); } From 1e7acec01799e3cfe6fd2a8630dbd8f6e3338251 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Oct 2024 10:31:01 +0000 Subject: [PATCH 002/508] Added Setup overlay app to communicate status of startup --- Jellyfin.Server/Program.cs | 25 +++- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 131 ++++++++++++++++++ .../Manager/NetworkManager.cs | 23 ++- 3 files changed, 167 insertions(+), 12 deletions(-) create mode 100644 Jellyfin.Server/ServerSetupApp/SetupServer.cs diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 295fb8112f..0bbcfa6a64 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -7,10 +7,13 @@ using System.Reflection; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; +using Jellyfin.Networking.Manager; using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; using Jellyfin.Server.Implementations; +using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; @@ -42,6 +45,9 @@ namespace Jellyfin.Server public const string LoggingConfigFileSystem = "logging.json"; private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); + private static SetupServer? _setupServer = new(); + + private static IHost? _jfHost = null; private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; @@ -68,6 +74,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); + await _setupServer!.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -122,6 +129,8 @@ namespace Jellyfin.Server if (_restartOnShutdown) { _startTimestamp = Stopwatch.GetTimestamp(); + _setupServer = new SetupServer(); + await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths).ConfigureAwait(false); } } while (_restartOnShutdown); } @@ -133,11 +142,9 @@ namespace Jellyfin.Server _loggerFactory, options, startupConfig); - - IHost? host = null; try { - host = Host.CreateDefaultBuilder() + _jfHost = Host.CreateDefaultBuilder() .UseConsoleLifetime() .ConfigureServices(services => appHost.Init(services)) .ConfigureWebHostDefaults(webHostBuilder => @@ -154,14 +161,18 @@ namespace Jellyfin.Server .Build(); // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. - appHost.ServiceProvider = host.Services; + appHost.ServiceProvider = _jfHost.Services; await appHost.InitializeServices().ConfigureAwait(false); Migrations.MigrationRunner.Run(appHost, _loggerFactory); try { - await host.StartAsync().ConfigureAwait(false); + await Task.Delay(50000).ConfigureAwait(false); + await _setupServer!.StopAsync().ConfigureAwait(false); + _setupServer.Dispose(); + _setupServer = null!; + await _jfHost.StartAsync().ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) { @@ -180,7 +191,7 @@ namespace Jellyfin.Server _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); - await host.WaitForShutdownAsync().ConfigureAwait(false); + await _jfHost.WaitForShutdownAsync().ConfigureAwait(false); _restartOnShutdown = appHost.ShouldRestart; } catch (Exception ex) @@ -205,7 +216,7 @@ namespace Jellyfin.Server } } - host?.Dispose(); + _jfHost?.Dispose(); } } diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs new file mode 100644 index 0000000000..61fe0fdd8c --- /dev/null +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -0,0 +1,131 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using SQLitePCL; + +namespace Jellyfin.Server.ServerSetupApp; + +/// +/// Creates a fake application pipeline that will only exist for as long as the main app is not started. +/// +public sealed class SetupServer : IDisposable +{ + private IHost? _startupServer; + private bool _disposed; + + /// + /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup. + /// + /// The networkmanager. + /// The application paths. + /// A Task. + public async Task RunAsync(Func networkManagerFactory, IApplicationPaths applicationPaths) + { + ThrowIfDisposed(); + _startupServer = Host.CreateDefaultBuilder() + .UseConsoleLifetime() + .ConfigureServices(serv => + { + serv.AddHealthChecks() + .AddCheck("StartupCheck"); + }) + .ConfigureWebHostDefaults(webHostBuilder => + { + webHostBuilder + .UseKestrel() + .Configure(app => + { + app.UseHealthChecks("/health"); + + app.Map("/startup/logger", loggerRoute => + { + loggerRoute.Run(async context => + { + var networkManager = networkManagerFactory(); + if (context.Connection.RemoteIpAddress is null || networkManager is null || !networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress)) + { + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return; + } + + var logfilePath = Directory.EnumerateFiles(applicationPaths.LogDirectoryPath).Select(e => new FileInfo(e)).OrderBy(f => f.CreationTimeUtc).FirstOrDefault()?.FullName; + if (logfilePath is not null) + { + await context.Response.SendFileAsync(logfilePath, CancellationToken.None).ConfigureAwait(false); + } + }); + }); + + app.Run((context) => + { + context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; + context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60"); + context.Response.WriteAsync("

Jellyfin Server still starting. Please wait.

"); + var networkManager = networkManagerFactory(); + if (networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress)) + { + context.Response.WriteAsync("

You can download the current logfiles here.

"); + } + + return Task.CompletedTask; + }); + }); + }) + .Build(); + await _startupServer.StartAsync().ConfigureAwait(false); + } + + /// + /// Stops the Setup server. + /// + /// A task. Duh. + public async Task StopAsync() + { + ThrowIfDisposed(); + if (_startupServer is null) + { + throw new InvalidOperationException("Tried to stop a non existing startup server"); + } + + await _startupServer.StopAsync().ConfigureAwait(false); + _startupServer.Dispose(); + } + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _startupServer?.Dispose(); + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + private class SetupHealthcheck : IHealthCheck + { + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up.")); + } + } +} diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 5a13cc4173..7a22dd8526 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -921,6 +921,19 @@ public class NetworkManager : INetworkManager, IDisposable /// public bool IsInLocalNetwork(IPAddress address) + { + return NetworkManager.IsInLocalNetwork(address, TrustAllIPv6Interfaces, _lanSubnets, _excludedSubnets); + } + + /// + /// Checks a ip address to match any lansubnet given but not to be in any excluded subnet. + /// + /// The IP address to checl. + /// Whenever all IPV6 subnet address shall be permitted. + /// The list of subnets to permit. + /// The list of subnets to never permit. + /// The check if the given IP address is in any provided subnet. + public static bool IsInLocalNetwork(IPAddress address, bool trustAllIpv6, IReadOnlyList lanSubnets, IReadOnlyList excludedSubnets) { ArgumentNullException.ThrowIfNull(address); @@ -930,23 +943,23 @@ public class NetworkManager : INetworkManager, IDisposable address = address.MapToIPv4(); } - if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + if ((trustAllIpv6 && address.AddressFamily == AddressFamily.InterNetworkV6) || IPAddress.IsLoopback(address)) { return true; } // As private addresses can be redefined by Configuration.LocalNetworkAddresses - return CheckIfLanAndNotExcluded(address); + return CheckIfLanAndNotExcluded(address, lanSubnets, excludedSubnets); } - private bool CheckIfLanAndNotExcluded(IPAddress address) + private static bool CheckIfLanAndNotExcluded(IPAddress address, IReadOnlyList lanSubnets, IReadOnlyList excludedSubnets) { - foreach (var lanSubnet in _lanSubnets) + foreach (var lanSubnet in lanSubnets) { if (lanSubnet.Contains(address)) { - foreach (var excludedSubnet in _excludedSubnets) + foreach (var excludedSubnet in excludedSubnets) { if (excludedSubnet.Contains(address)) { From cd81a698a6020a5ab4aa469e2350cbcc4e09e8a4 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Oct 2024 10:34:11 +0000 Subject: [PATCH 003/508] Reverted change to network manager --- .../Manager/NetworkManager.cs | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 7a22dd8526..5a13cc4173 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -921,19 +921,6 @@ public class NetworkManager : INetworkManager, IDisposable /// public bool IsInLocalNetwork(IPAddress address) - { - return NetworkManager.IsInLocalNetwork(address, TrustAllIPv6Interfaces, _lanSubnets, _excludedSubnets); - } - - /// - /// Checks a ip address to match any lansubnet given but not to be in any excluded subnet. - /// - /// The IP address to checl. - /// Whenever all IPV6 subnet address shall be permitted. - /// The list of subnets to permit. - /// The list of subnets to never permit. - /// The check if the given IP address is in any provided subnet. - public static bool IsInLocalNetwork(IPAddress address, bool trustAllIpv6, IReadOnlyList lanSubnets, IReadOnlyList excludedSubnets) { ArgumentNullException.ThrowIfNull(address); @@ -943,23 +930,23 @@ public class NetworkManager : INetworkManager, IDisposable address = address.MapToIPv4(); } - if ((trustAllIpv6 && address.AddressFamily == AddressFamily.InterNetworkV6) + if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) || IPAddress.IsLoopback(address)) { return true; } // As private addresses can be redefined by Configuration.LocalNetworkAddresses - return CheckIfLanAndNotExcluded(address, lanSubnets, excludedSubnets); + return CheckIfLanAndNotExcluded(address); } - private static bool CheckIfLanAndNotExcluded(IPAddress address, IReadOnlyList lanSubnets, IReadOnlyList excludedSubnets) + private bool CheckIfLanAndNotExcluded(IPAddress address) { - foreach (var lanSubnet in lanSubnets) + foreach (var lanSubnet in _lanSubnets) { if (lanSubnet.Contains(address)) { - foreach (var excludedSubnet in excludedSubnets) + foreach (var excludedSubnet in _excludedSubnets) { if (excludedSubnet.Contains(address)) { From ebabaac6b1c4eca7203f4b477fa5e79bd786760c Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Oct 2024 10:47:38 +0000 Subject: [PATCH 004/508] removed dbg timeout --- Jellyfin.Server/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 0bbcfa6a64..a3b16d6a7d 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -168,7 +168,6 @@ namespace Jellyfin.Server try { - await Task.Delay(50000).ConfigureAwait(false); await _setupServer!.StopAsync().ConfigureAwait(false); _setupServer.Dispose(); _setupServer = null!; From dc029d549c0da8e0747d46f51a06621a16eb61df Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Oct 2024 11:08:20 +0000 Subject: [PATCH 005/508] removed double dispose --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 61fe0fdd8c..fc0680e40f 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -101,7 +101,6 @@ public sealed class SetupServer : IDisposable } await _startupServer.StopAsync().ConfigureAwait(false); - _startupServer.Dispose(); } /// From 41c27d4e7e197308f3ff978c59e538028bbf4ef4 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Oct 2024 16:59:12 +0000 Subject: [PATCH 006/508] ATV requested endpoint mock --- Jellyfin.Server/Program.cs | 18 ++++---- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 43 ++++++++++++++++++- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index a3b16d6a7d..922a06802a 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -46,7 +46,7 @@ namespace Jellyfin.Server private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static SetupServer? _setupServer = new(); - + private static CoreAppHost? _appHost; private static IHost? _jfHost = null; private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; @@ -74,7 +74,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); - await _setupServer!.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths).ConfigureAwait(false); + await _setupServer!.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -130,18 +130,19 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); _setupServer = new SetupServer(); - await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths).ConfigureAwait(false); + await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); } } while (_restartOnShutdown); } private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) { - using var appHost = new CoreAppHost( - appPaths, - _loggerFactory, - options, - startupConfig); + using CoreAppHost appHost = new CoreAppHost( + appPaths, + _loggerFactory, + options, + startupConfig); + _appHost = appHost; try { _jfHost = Host.CreateDefaultBuilder() @@ -215,6 +216,7 @@ namespace Jellyfin.Server } } + _appHost = null; _jfHost?.Dispose(); } } diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index fc0680e40f..ea4804753b 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -7,6 +7,8 @@ using System.Threading.Tasks; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Model.System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Hosting; @@ -31,8 +33,12 @@ public sealed class SetupServer : IDisposable ///
/// The networkmanager. /// The application paths. + /// The servers application host. /// A Task. - public async Task RunAsync(Func networkManagerFactory, IApplicationPaths applicationPaths) + public async Task RunAsync( + Func networkManagerFactory, + IApplicationPaths applicationPaths, + Func serverApplicationHost) { ThrowIfDisposed(); _startupServer = Host.CreateDefaultBuilder() @@ -69,6 +75,41 @@ public sealed class SetupServer : IDisposable }); }); + app.Map("/System/Info/Public", systemRoute => + { + systemRoute.Run(async context => + { + var jfApplicationHost = serverApplicationHost(); + + var retryCounter = 0; + while (jfApplicationHost is null && retryCounter < 5) + { + await Task.Delay(500).ConfigureAwait(false); + jfApplicationHost = serverApplicationHost(); + retryCounter++; + } + + if (jfApplicationHost is null) + { + context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; + context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60"); + return; + } + + var sysInfo = new PublicSystemInfo + { + Version = jfApplicationHost.ApplicationVersionString, + ProductName = jfApplicationHost.Name, + Id = jfApplicationHost.SystemId, + ServerName = jfApplicationHost.FriendlyName, + LocalAddress = jfApplicationHost.GetSmartApiUrl(context.Request), + StartupWizardCompleted = false + }; + + await context.Response.WriteAsJsonAsync(sysInfo).ConfigureAwait(false); + }); + }); + app.Run((context) => { context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; From 12c14ddb24093578e28142b76aa2b1bbb9dfda11 Mon Sep 17 00:00:00 2001 From: SenorSmartyPants Date: Sun, 1 Dec 2024 22:08:15 -0600 Subject: [PATCH 007/508] Round CommunityRating to nearest tenths --- MediaBrowser.Model/Extensions/EnumerableExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs index 8963bdb731..94f4252295 100644 --- a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs +++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs @@ -50,7 +50,7 @@ namespace MediaBrowser.Model.Extensions return 0; }) - .ThenByDescending(i => i.CommunityRating ?? 0) + .ThenByDescending(i => Math.Round(i.CommunityRating ?? 0, 1) ) .ThenByDescending(i => i.VoteCount ?? 0); } } From 0fc288936d10afc146780d118361f2e722768ee6 Mon Sep 17 00:00:00 2001 From: gnattu Date: Mon, 9 Dec 2024 16:17:49 +0800 Subject: [PATCH 008/508] Enable VideoToolbox AV1 decode This decoder differs from others provided by VideoToolbox in that it lacks any software fallback. To achieve consistent behavior with other VideoToolbox decoders, this PR implemented additional checking on the server to simulate the software fallback provided by VideoToolbox. The current fallback checking mechanism is a temporary solution. In the long term, it should be replaced with a more capable hardware capability checking system. --- .../MediaEncoding/EncodingHelper.cs | 8 ++ .../MediaEncoding/IMediaEncoder.cs | 6 ++ .../Encoder/ApplePlatformHelper.cs | 85 +++++++++++++++++++ .../Encoder/EncoderValidator.cs | 5 ++ .../Encoder/MediaEncoder.cs | 10 +++ 5 files changed, 114 insertions(+) create mode 100644 MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 9399679a4f..7dea5f8ebd 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -6610,6 +6610,7 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuv422p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase) || string.Equals("yuv444p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); + var isAv1SupportedSwFormatsVt = is8_10bitSwFormatsVt || string.Equals("yuv420p12le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase); // The related patches make videotoolbox hardware surface working is only available in jellyfin-ffmpeg 7.0.1 at the moment. bool useHwSurface = (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface) && IsVideoToolboxFullSupported(); @@ -6643,6 +6644,13 @@ namespace MediaBrowser.Controller.MediaEncoding { return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface); } + + if (string.Equals("av1", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + && isAv1SupportedSwFormatsVt + && _mediaEncoder.IsVideoToolboxAv1DecodeAvailable) + { + return GetHwaccelType(state, options, "av1", bitDepth, useHwSurface); + } } return null; diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index c767b4a519..a60f523408 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -75,6 +75,12 @@ namespace MediaBrowser.Controller.MediaEncoding /// true if the Vaapi device supports vulkan drm interop, false otherwise. bool IsVaapiDeviceSupportVulkanDrmInterop { get; } + /// + /// Gets a value indicating whether av1 decoding is available via VideoToolbox. + /// + /// true if the av1 is available via VideoToolbox, false otherwise. + bool IsVideoToolboxAv1DecodeAvailable { get; } + /// /// Whether given encoder codec is supported. /// diff --git a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs new file mode 100644 index 0000000000..ea2289bd7d --- /dev/null +++ b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs @@ -0,0 +1,85 @@ +#pragma warning disable CA1031 + +using System; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.MediaEncoding.Encoder; + +/// +/// Helper class for Apple platform specific operations. +/// +public static class ApplePlatformHelper +{ + private static readonly string[] _av1DecodeBlacklistedCpuClass = ["M1", "M2"]; + + private static string GetSysctlValue(string name) + { + IntPtr length = IntPtr.Zero; + // Get length of the value + int osStatus = SysctlByName(name, IntPtr.Zero, ref length, IntPtr.Zero, 0); + + if (osStatus != 0) + { + throw new NotSupportedException($"Failed to get sysctl value for {name} with error {osStatus}"); + } + + IntPtr buffer = Marshal.AllocHGlobal(length.ToInt32()); + try + { + osStatus = SysctlByName(name, buffer, ref length, IntPtr.Zero, 0); + if (osStatus != 0) + { + throw new NotSupportedException($"Failed to get sysctl value for {name} with error {osStatus}"); + } + + return Marshal.PtrToStringAnsi(buffer) ?? string.Empty; + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + + private static int SysctlByName(string name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen) + { + return NativeMethods.SysctlByName(System.Text.Encoding.ASCII.GetBytes(name), oldp, ref oldlenp, newp, newlen); + } + + /// + /// Check if the current system has hardware acceleration for AV1 decoding. + /// + /// The logger used for error logging. + /// Boolean indicates the hwaccel support. + public static bool HasAv1HardwareAccel(ILogger logger) + { + if (!RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)) + { + return false; + } + + try + { + string cpuBrandString = GetSysctlValue("machdep.cpu.brand_string"); + return !_av1DecodeBlacklistedCpuClass.Any(blacklistedCpuClass => cpuBrandString.Contains(blacklistedCpuClass, StringComparison.OrdinalIgnoreCase)); + } + catch (NotSupportedException e) + { + logger.LogError("Error getting CPU brand string: {Message}", e.Message); + } + catch (Exception e) + { + logger.LogError("Unknown error occured: {Exception}", e); + } + + return false; + } + + private static class NativeMethods + { + [DllImport("libc", EntryPoint = "sysctlbyname", SetLastError = true)] + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + internal static extern int SysctlByName(byte[] name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen); + } +} diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 23d9ca7ef5..776b2ab42c 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -437,6 +437,11 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + public bool CheckIsVideoToolboxAv1DecodeAvailable() + { + return ApplePlatformHelper.HasAv1HardwareAccel(_logger); + } + private IEnumerable GetHwaccelTypes() { string? output = null; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index a34238cd68..44b38f03bc 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -83,6 +83,8 @@ namespace MediaBrowser.MediaEncoding.Encoder private bool _isVaapiDeviceSupportVulkanDrmModifier = false; private bool _isVaapiDeviceSupportVulkanDrmInterop = false; + private bool _isVideoToolboxAv1DecodeAvailable = false; + private static string[] _vulkanImageDrmFmtModifierExts = { "VK_EXT_image_drm_format_modifier", @@ -153,6 +155,8 @@ namespace MediaBrowser.MediaEncoding.Encoder /// public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop; + public bool IsVideoToolboxAv1DecodeAvailable => _isVideoToolboxAv1DecodeAvailable; + [GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")] private static partial Regex FfprobePathRegex(); @@ -255,6 +259,12 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.VaapiDevice); } } + + // Check if VideoToolbox supports AV1 decode + if (OperatingSystem.IsMacOS() && SupportsHwaccel("videotoolbox")) + { + _isVideoToolboxAv1DecodeAvailable = validator.CheckIsVideoToolboxAv1DecodeAvailable(); + } } _logger.LogInformation("FFmpeg: {FfmpegPath}", _ffmpegPath ?? string.Empty); From e16ea7b23696a49b96bcd9a8e81cd23db470524b Mon Sep 17 00:00:00 2001 From: Qian Qian Date: Wed, 1 Jan 2025 11:47:14 +0800 Subject: [PATCH 009/508] always sort season by index number --- MediaBrowser.Controller/Entities/TV/Series.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index a324f79eff..3e0e7b1c5b 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -213,7 +213,7 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; query.IncludeItemTypes = new[] { BaseItemKind.Season }; - query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; + query.OrderBy = new[] { (ItemSortBy.IndexNumber, SortOrder.Ascending) }; if (user is not null && !user.DisplayMissingEpisodes) { @@ -231,10 +231,6 @@ namespace MediaBrowser.Controller.Entities.TV query.AncestorWithPresentationUniqueKey = null; query.SeriesPresentationUniqueKey = seriesKey; - if (query.OrderBy.Count == 0) - { - query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }; - } if (query.IncludeItemTypes.Length == 0) { From 294190bb21350810890f9bcbd3edac78c25355be Mon Sep 17 00:00:00 2001 From: felix920506 Date: Thu, 16 Jan 2025 08:05:56 -0500 Subject: [PATCH 010/508] Update pull-request-conflict.yml --- .github/workflows/pull-request-conflict.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml index 411ebf8290..e6a9bf0caa 100644 --- a/.github/workflows/pull-request-conflict.yml +++ b/.github/workflows/pull-request-conflict.yml @@ -12,7 +12,7 @@ jobs: label: name: Labeling runs-on: ubuntu-latest - if: ${{ github.repository == 'jellyfin/jellyfin' }} + if: ${{ github.repository == 'jellyfin/jellyfin' && github.event.issue.pull_request }} steps: - name: Apply label uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 From 6454a35ef831157fb10d8cbdf39017b2df2b8449 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 22 Jan 2025 18:20:57 +0100 Subject: [PATCH 011/508] Extract trickplay files into own subdirectory --- .../AppBase/BaseApplicationPaths.cs | 58 +++++-------------- .../Trickplay/TrickplayManager.cs | 4 +- .../Migrations/Routines/MoveTrickplayFiles.cs | 24 +++++++- .../Configuration/IApplicationPaths.cs | 6 ++ 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index dc845b2d7e..f0cca9efd0 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -34,76 +34,46 @@ namespace Emby.Server.Implementations.AppBase DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; } - /// - /// Gets the path to the program data folder. - /// - /// The program data path. + /// public string ProgramDataPath { get; } /// public string WebPath { get; } - /// - /// Gets the path to the system folder. - /// - /// The path to the system folder. + /// public string ProgramSystemPath { get; } = AppContext.BaseDirectory; - /// - /// Gets the folder path to the data directory. - /// - /// The data directory. + /// public string DataPath { get; } /// public string VirtualDataPath => "%AppDataPath%"; - /// - /// Gets the image cache path. - /// - /// The image cache path. + /// public string ImageCachePath => Path.Combine(CachePath, "images"); - /// - /// Gets the path to the plugin directory. - /// - /// The plugins path. + /// public string PluginsPath => Path.Combine(ProgramDataPath, "plugins"); - /// - /// Gets the path to the plugin configurations directory. - /// - /// The plugin configurations path. + /// public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations"); - /// - /// Gets the path to the log directory. - /// - /// The log directory path. + /// public string LogDirectoryPath { get; } - /// - /// Gets the path to the application configuration root directory. - /// - /// The configuration directory path. + /// public string ConfigurationDirectoryPath { get; } - /// - /// Gets the path to the system configuration file. - /// - /// The system configuration file path. + /// public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml"); - /// - /// Gets or sets the folder path to the cache directory. - /// - /// The cache directory. + /// public string CachePath { get; set; } - /// - /// Gets the folder path to the temp directory within the cache folder. - /// - /// The temp directory. + /// public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin"); + + /// + public string TrickplayPath => Path.Combine(DataPath, "trickplay"); } } diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index cd73d67c3b..e94673bcec 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -602,9 +602,11 @@ public class TrickplayManager : ITrickplayManager /// public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false) { + var basePath = _config.ApplicationPaths.TrickplayPath; + var idString = item.Id.ToString("N", CultureInfo.InvariantCulture); var path = saveWithMedia ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) - : Path.Combine(item.GetInternalMetadataPath(), "trickplay"); + : Path.Combine(basePath, idString); var subdirectory = string.Format( CultureInfo.InvariantCulture, diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs index c1a9e88949..f4ebac3778 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs @@ -39,7 +39,7 @@ public class MoveTrickplayFiles : IMigrationRoutine } /// - public Guid Id => new("4EF123D5-8EFF-4B0B-869D-3AED07A60E1B"); + public Guid Id => new("9540D44A-D8DC-11EF-9CBB-B77274F77C52"); /// public string Name => "MoveTrickplayFiles"; @@ -89,6 +89,12 @@ public class MoveTrickplayFiles : IMigrationRoutine { _fileSystem.MoveDirectory(oldPath, newPath); } + + oldPath = GetNewOldTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, trickplayInfo.Width, false); + if (_fileSystem.DirectoryExists(oldPath)) + { + _fileSystem.MoveDirectory(oldPath, newPath); + } } } while (previousCount == Limit); @@ -101,4 +107,20 @@ public class MoveTrickplayFiles : IMigrationRoutine return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; } + + private string GetNewOldTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false) + { + var path = saveWithMedia + ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) + : Path.Combine(item.GetInternalMetadataPath(), "trickplay"); + + var subdirectory = string.Format( + CultureInfo.InvariantCulture, + "{0} - {1}x{2}", + width.ToString(CultureInfo.InvariantCulture), + tileWidth.ToString(CultureInfo.InvariantCulture), + tileHeight.ToString(CultureInfo.InvariantCulture)); + + return Path.Combine(path, subdirectory); + } } diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index 57c6546675..7a8ab32361 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -84,5 +84,11 @@ namespace MediaBrowser.Common.Configuration /// /// The magic string used for virtual path manipulation. string VirtualDataPath { get; } + + /// + /// Gets the path used for storing trickplay files. + /// + /// The trickplay path. + string TrickplayPath { get; } } } From aa811eb1e3c78bdf8f4a751311c1bb6d639e851e Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 26 Jan 2025 20:45:28 +0000 Subject: [PATCH 012/508] Prepared Seperation of Database components for future multi provider support --- .devcontainer/devcontainer.json | 14 ++ Directory.Packages.props | 1 + .../Emby.Server.Implementations.csproj | 1 + .../HttpServer/Security/AuthService.cs | 1 + .../Library/LibraryManager.cs | 1 + .../Library/MediaSourceManager.cs | 1 + .../Library/UserViewManager.cs | 1 + .../Tasks/OptimizeDatabaseTask.cs | 21 +- .../Session/SessionManager.cs | 1 + .../TV/TVSeriesManager.cs | 1 + .../Auth/CustomAuthenticationHandler.cs | 1 + .../DefaultAuthorizationHandler.cs | 1 + .../UserPermissionHandler.cs | 1 + Jellyfin.Api/Controllers/ItemsController.cs | 1 + Jellyfin.Api/Controllers/UserController.cs | 1 + Jellyfin.Api/Helpers/MediaInfoHelper.cs | 1 + .../ActivityLogWebSocketListener.cs | 1 + .../SessionInfoWebSocketListener.cs | 1 + Jellyfin.Data/Interfaces/IHasPermissions.cs | 31 --- Jellyfin.Data/Jellyfin.Data.csproj | 4 + Jellyfin.Data/UserEntityExtensions.cs | 220 ++++++++++++++++++ .../Entities/AccessSchedule.cs | 0 .../Entities/ActivityLog.cs | 0 .../Entities/AncestorId.cs | 0 .../Entities/AttachmentStreamInfo.cs | 0 .../Entities/BaseItemEntity.cs | 0 .../Entities/BaseItemExtraType.cs | 0 .../Entities/BaseItemImageInfo.cs | 0 .../Entities/BaseItemMetadataField.cs | 0 .../Entities/BaseItemProvider.cs | 0 .../Entities/BaseItemTrailerType.cs | 0 .../Entities/Chapter.cs | 0 .../Entities/CustomItemDisplayPreferences.cs | 0 .../Entities/DisplayPreferences.cs | 0 .../Entities/Group.cs | 12 - .../Entities/HomeSection.cs | 0 .../Entities/ImageInfo.cs | 0 .../Entities/ImageInfoImageType.cs | 0 .../Entities/ItemDisplayPreferences.cs | 0 .../Entities/ItemValue.cs | 0 .../Entities/ItemValueMap.cs | 0 .../Entities/ItemValueType.cs | 0 .../Entities/Libraries/Artwork.cs | 0 .../Entities/Libraries/Book.cs | 0 .../Entities/Libraries/BookMetadata.cs | 0 .../Entities/Libraries/Chapter.cs | 0 .../Entities/Libraries/Collection.cs | 0 .../Entities/Libraries/CollectionItem.cs | 0 .../Entities/Libraries/Company.cs | 0 .../Entities/Libraries/CompanyMetadata.cs | 0 .../Entities/Libraries/CustomItem.cs | 0 .../Entities/Libraries/CustomItemMetadata.cs | 0 .../Entities/Libraries/Episode.cs | 0 .../Entities/Libraries/EpisodeMetadata.cs | 0 .../Entities/Libraries/Genre.cs | 0 .../Entities/Libraries/ItemMetadata.cs | 0 .../Entities/Libraries/Library.cs | 0 .../Entities/Libraries/LibraryItem.cs | 0 .../Entities/Libraries/MediaFile.cs | 0 .../Entities/Libraries/MediaFileStream.cs | 0 .../Entities/Libraries/MetadataProvider.cs | 0 .../Entities/Libraries/MetadataProviderId.cs | 0 .../Entities/Libraries/Movie.cs | 0 .../Entities/Libraries/MovieMetadata.cs | 0 .../Entities/Libraries/MusicAlbum.cs | 0 .../Entities/Libraries/MusicAlbumMetadata.cs | 0 .../Entities/Libraries/Person.cs | 0 .../Entities/Libraries/PersonRole.cs | 0 .../Entities/Libraries/Photo.cs | 0 .../Entities/Libraries/PhotoMetadata.cs | 0 .../Entities/Libraries/Rating.cs | 0 .../Entities/Libraries/RatingSource.cs | 0 .../Entities/Libraries/Release.cs | 0 .../Entities/Libraries/Season.cs | 0 .../Entities/Libraries/SeasonMetadata.cs | 0 .../Entities/Libraries/Series.cs | 0 .../Entities/Libraries/SeriesMetadata.cs | 0 .../Entities/Libraries/Track.cs | 0 .../Entities/Libraries/TrackMetadata.cs | 0 .../Entities/MediaSegment.cs | 0 .../Entities/MediaStreamInfo.cs | 0 .../Entities/MediaStreamTypeEntity.cs | 0 .../Entities/People.cs | 0 .../Entities/PeopleBaseItemMap.cs | 0 .../Entities/Permission.cs | 0 .../Entities/Preference.cs | 0 .../Entities/ProgramAudioEntity.cs | 0 .../Entities/Security/ApiKey.cs | 0 .../Entities/Security/Device.cs | 0 .../Entities/Security/DeviceOptions.cs | 0 .../Entities/TrickplayInfo.cs | 0 .../Entities/User.cs | 196 ---------------- .../Entities/UserData.cs | 0 .../Enums/ArtKind.cs | 0 .../Enums/ChromecastVersion.cs | 0 .../Enums/DynamicDayOfWeek.cs | 0 .../Enums/HomeSectionType.cs | 0 .../Enums/IndexingKind.cs | 0 .../Enums/MediaFileKind.cs | 0 .../Enums/MediaSegmentType.cs | 0 .../Enums/PermissionKind.cs | 0 .../Enums/PersonRoleType.cs | 0 .../Enums/PreferenceKind.cs | 0 .../Enums/ScrollDirection.cs | 0 .../Enums/SortOrder.cs | 0 .../Enums/SubtitlePlaybackMode.cs | 0 .../Enums/SyncPlayUserAccessType.cs | 0 .../Enums/ViewType.cs | 0 .../IJellyfinDatabaseProvider.cs | 31 +++ .../Interfaces/IHasArtwork.cs | 0 .../Interfaces/IHasCompanies.cs | 0 .../Interfaces/IHasConcurrencyToken.cs | 0 .../Interfaces/IHasPermissions.cs | 17 ++ .../Interfaces/IHasReleases.cs | 0 .../Jellyfin.Database.Implementations.csproj | 43 ++++ .../JellyfinDatabaseProviderKeyAttribute.cs | 29 +++ .../JellyfinDbContext.cs | 5 +- .../ActivityLogConfiguration.cs | 0 .../AncestorIdConfiguration.cs | 0 .../ModelConfiguration/ApiKeyConfiguration.cs | 0 .../AttachmentStreamInfoConfiguration.cs | 0 .../BaseItemConfiguration.cs | 2 - .../BaseItemMetadataFieldConfiguration.cs | 4 - .../BaseItemProviderConfiguration.cs | 0 .../BaseItemTrailerTypeConfiguration.cs | 4 - .../ChapterConfiguration.cs | 0 ...stomItemDisplayPreferencesConfiguration.cs | 0 .../ModelConfiguration/DeviceConfiguration.cs | 0 .../DeviceOptionsConfiguration.cs | 0 .../DisplayPreferencesConfiguration.cs | 0 .../ItemValuesConfiguration.cs | 0 .../ItemValuesMapConfiguration.cs | 0 .../MediaStreamInfoConfiguration.cs | 0 .../PeopleBaseItemMapConfiguration.cs | 0 .../ModelConfiguration/PeopleConfiguration.cs | 0 .../PermissionConfiguration.cs | 0 .../PreferenceConfiguration.cs | 0 .../TrickplayInfoConfiguration.cs | 0 .../ModelConfiguration/UserConfiguration.cs | 0 .../UserDataConfiguration.cs | 0 .../Jellyfin.Database.Providers.PgSql.csproj | 51 ++++ .../Jellyfin.Database.Providers.SqLite.csproj | 51 ++++ .../20200514181226_AddActivityLog.Designer.cs | 0 .../20200514181226_AddActivityLog.cs | 0 .../20200613202153_AddUsers.Designer.cs | 0 .../Migrations/20200613202153_AddUsers.cs | 0 ...28005145_AddDisplayPreferences.Designer.cs | 0 .../20200728005145_AddDisplayPreferences.cs | 0 ...533_FixDisplayPreferencesIndex.Designer.cs | 0 ...200905220533_FixDisplayPreferencesIndex.cs | 0 ...004171403_AddMaxActiveSessions.Designer.cs | 0 .../20201004171403_AddMaxActiveSessions.cs | 0 ...55_AddCustomDisplayPreferences.Designer.cs | 0 ...01204223655_AddCustomDisplayPreferences.cs | 0 ...181425_AddIndexesAndCollations.Designer.cs | 0 .../20210320181425_AddIndexesAndCollations.cs | 0 ...110544_NullableCustomPrefValue.Designer.cs | 0 .../20210407110544_NullableCustomPrefValue.cs | 0 .../20210814002109_AddDevices.Designer.cs | 0 .../Migrations/20210814002109_AddDevices.cs | 0 ...ddIndexActivityLogsDateCreated.Designer.cs | 0 ...2080052_AddIndexActivityLogsDateCreated.cs | 0 ...30526173516_RemoveEasyPassword.Designer.cs | 0 .../20230526173516_RemoveEasyPassword.cs | 0 ...230626233818_AddTrickplayInfos.Designer.cs | 0 .../20230626233818_AddTrickplayInfos.cs | 0 ...0230923170422_UserCastReceiver.Designer.cs | 0 .../20230923170422_UserCastReceiver.cs | 0 ...0240729140605_AddMediaSegments.Designer.cs | 0 .../20240729140605_AddMediaSegments.cs | 0 ...rkSegmentProviderIdNonNullable.Designer.cs | 0 ...082930_MarkSegmentProviderIdNonNullable.cs | 0 ...41020103111_LibraryDbMigration.Designer.cs | 0 .../20241020103111_LibraryDbMigration.cs | 0 ...41111131257_AddedCustomDataKey.Designer.cs | 0 .../20241111131257_AddedCustomDataKey.cs | 0 ...11135439_AddedCustomDataKeyKey.Designer.cs | 0 .../20241111135439_AddedCustomDataKeyKey.cs | 0 ...1112152323_FixAncestorIdConfig.Designer.cs | 0 .../20241112152323_FixAncestorIdConfig.cs | 0 ...20241112232041_fixMediaStreams.Designer.cs | 0 .../20241112232041_fixMediaStreams.cs | 0 ...0241112234144_FixMediaStreams2.Designer.cs | 0 .../20241112234144_FixMediaStreams2.cs | 0 ...3133548_EnforceUniqueItemValue.Designer.cs | 0 .../20241113133548_EnforceUniqueItemValue.cs | 0 .../Migrations/DesignTimeJellyfinDbFactory.cs | 8 +- .../Migrations/JellyfinDbModelSnapshot.cs | 0 .../SqliteDatabaseProvider.cs | 78 +++++++ .../DatabaseConfigurationFactory.cs | 17 ++ .../DatabaseConfigurationOptions.cs | 14 ++ .../DatabaseConfigurationStore.cs | 25 ++ .../Devices/DeviceManager.cs | 1 + .../Extensions/ServiceCollectionExtensions.cs | 54 ++++- .../Jellyfin.Server.Implementations.csproj | 10 +- .../Users/DeviceAccessHost.cs | 1 + .../Users/UserManager.cs | 1 + Jellyfin.Server/Jellyfin.Server.csproj | 1 + .../Migrations/Routines/MigrateLibraryDb.cs | 19 +- .../Migrations/Routines/MigrateUserDb.cs | 1 + Jellyfin.Server/Program.cs | 18 +- Jellyfin.Server/Startup.cs | 2 +- Jellyfin.sln | 29 ++- MediaBrowser.Controller/Channels/Channel.cs | 1 + .../Entities/Audio/MusicAlbum.cs | 1 + .../Entities/Audio/MusicArtist.cs | 1 + MediaBrowser.Controller/Entities/BaseItem.cs | 1 + MediaBrowser.Controller/Entities/Folder.cs | 1 + .../Entities/InternalItemsQuery.cs | 1 + .../Entities/Movies/BoxSet.cs | 1 + MediaBrowser.Controller/Entities/TV/Series.cs | 2 +- .../Entities/UserViewBuilder.cs | 1 + .../MediaEncoding/EncodingHelper.cs | 1 + MediaBrowser.Controller/Playlists/Playlist.cs | 1 + .../Transcoding/TranscodeManager.cs | 1 + src/Jellyfin.LiveTv/LiveTvManager.cs | 1 + .../Recordings/RecordingNotifier.cs | 1 + .../Auth/CustomAuthenticationHandlerTests.cs | 1 + tests/Jellyfin.Api.Tests/TestHelpers.cs | 1 + .../EfMigrations/EfMigrationTests.cs | 15 +- 220 files changed, 742 insertions(+), 320 deletions(-) delete mode 100644 Jellyfin.Data/Interfaces/IHasPermissions.cs create mode 100644 Jellyfin.Data/UserEntityExtensions.cs rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/AccessSchedule.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/ActivityLog.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/AncestorId.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/AttachmentStreamInfo.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/BaseItemEntity.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/BaseItemExtraType.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/BaseItemImageInfo.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/BaseItemMetadataField.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/BaseItemProvider.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/BaseItemTrailerType.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Chapter.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/CustomItemDisplayPreferences.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/DisplayPreferences.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Group.cs (84%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/HomeSection.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/ImageInfo.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/ImageInfoImageType.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/ItemDisplayPreferences.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/ItemValue.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/ItemValueMap.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/ItemValueType.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Artwork.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Book.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/BookMetadata.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Chapter.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Collection.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/CollectionItem.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Company.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/CompanyMetadata.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/CustomItem.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/CustomItemMetadata.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Episode.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/EpisodeMetadata.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Genre.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/ItemMetadata.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Library.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/LibraryItem.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/MediaFile.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/MediaFileStream.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/MetadataProvider.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/MetadataProviderId.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Movie.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/MovieMetadata.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/MusicAlbum.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/MusicAlbumMetadata.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Person.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/PersonRole.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Photo.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/PhotoMetadata.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Rating.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/RatingSource.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Release.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Season.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/SeasonMetadata.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Series.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/SeriesMetadata.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/Track.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Libraries/TrackMetadata.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/MediaSegment.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/MediaStreamInfo.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/MediaStreamTypeEntity.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/People.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/PeopleBaseItemMap.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Permission.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Preference.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/ProgramAudioEntity.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Security/ApiKey.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Security/Device.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/Security/DeviceOptions.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/TrickplayInfo.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/User.cs (56%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Entities/UserData.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/ArtKind.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/ChromecastVersion.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/DynamicDayOfWeek.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/HomeSectionType.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/IndexingKind.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/MediaFileKind.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/MediaSegmentType.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/PermissionKind.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/PersonRoleType.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/PreferenceKind.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/ScrollDirection.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/SortOrder.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/SubtitlePlaybackMode.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/SyncPlayUserAccessType.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Enums/ViewType.cs (100%) create mode 100644 Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Interfaces/IHasArtwork.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Interfaces/IHasCompanies.cs (100%) rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Interfaces/IHasConcurrencyToken.cs (100%) create mode 100644 Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs rename {Jellyfin.Data => Jellyfin.Database/Jellyfin.Database.Implementations}/Interfaces/IHasReleases.cs (100%) create mode 100644 Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj create mode 100644 Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/JellyfinDbContext.cs (96%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/ActivityLogConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/AncestorIdConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/ApiKeyConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/AttachmentStreamInfoConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/BaseItemConfiguration.cs (98%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs (87%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/BaseItemProviderConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs (87%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/ChapterConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/DeviceConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/DeviceOptionsConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/DisplayPreferencesConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/ItemValuesConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/ItemValuesMapConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/MediaStreamInfoConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/PeopleBaseItemMapConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/PeopleConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/PermissionConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/PreferenceConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/TrickplayInfoConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/UserConfiguration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/ModelConfiguration/UserDataConfiguration.cs (100%) create mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Jellyfin.Database.Providers.PgSql.csproj create mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20200514181226_AddActivityLog.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20200514181226_AddActivityLog.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20200613202153_AddUsers.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20200613202153_AddUsers.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20200728005145_AddDisplayPreferences.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20200728005145_AddDisplayPreferences.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20200905220533_FixDisplayPreferencesIndex.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20201004171403_AddMaxActiveSessions.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20201204223655_AddCustomDisplayPreferences.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20210320181425_AddIndexesAndCollations.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20210407110544_NullableCustomPrefValue.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20210814002109_AddDevices.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20210814002109_AddDevices.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20230526173516_RemoveEasyPassword.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20230526173516_RemoveEasyPassword.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20230626233818_AddTrickplayInfos.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20230626233818_AddTrickplayInfos.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20230923170422_UserCastReceiver.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20230923170422_UserCastReceiver.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20240729140605_AddMediaSegments.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20240729140605_AddMediaSegments.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241020103111_LibraryDbMigration.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241020103111_LibraryDbMigration.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241111131257_AddedCustomDataKey.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241111131257_AddedCustomDataKey.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241111135439_AddedCustomDataKeyKey.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241112152323_FixAncestorIdConfig.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241112232041_fixMediaStreams.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241112232041_fixMediaStreams.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241112234144_FixMediaStreams2.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241112234144_FixMediaStreams2.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/20241113133548_EnforceUniqueItemValue.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/DesignTimeJellyfinDbFactory.cs (61%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/Migrations/JellyfinDbModelSnapshot.cs (100%) create mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs create mode 100644 Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationFactory.cs create mode 100644 Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs create mode 100644 Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationStore.cs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 228d4a17c8..bcf484463b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,5 +24,19 @@ "hostRequirements": { "memory": "8gb", "cpus": 4 + }, "remoteEnv": { + "JELLYFIN_DATA_DIR": "/config" + }, + "mounts": [ + "source=/opt/docker/data/jellyfin/testConfig/,target=/config,type=bind,consistency=cached", + "source=/opt/docker/data/jellyfin/config10.9.11/metadata,target=/config/metadata,type=bind,consistency=cached", + "source=/mnt/video,target=/media,type=bind,consistency=cached" + ], + "customizations": { + "vscode": { + "extensions": [ + "alexcvzz.vscode-sqlite" + ] + } } } diff --git a/Directory.Packages.props b/Directory.Packages.props index c85d0c0328..526ca37708 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -31,6 +31,7 @@ + diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 70dd5eb9ae..c94ff924c5 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -18,6 +18,7 @@ + diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 1d04f3da37..82945a4f62 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 93ee47fe81..1fc9ccb141 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -18,6 +18,7 @@ using Emby.Server.Implementations.Library.Validators; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks.Tasks; using Emby.Server.Implementations.Sorting; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index d0f5e60f79..669db65f7d 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -13,6 +13,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index e9cf47d462..b4e05ebf03 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs index 7d4e2377dc..05223d28ae 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs @@ -18,6 +18,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks private readonly ILogger _logger; private readonly ILocalizationManager _localization; private readonly IDbContextFactory _provider; + private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider; /// /// Initializes a new instance of the class. @@ -25,14 +26,17 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. + /// Instance of the JellyfinDatabaseProvider that can be used for provider specific operations. public OptimizeDatabaseTask( ILogger logger, ILocalizationManager localization, - IDbContextFactory provider) + IDbContextFactory provider, + IJellyfinDatabaseProvider jellyfinDatabaseProvider) { _logger = logger; _localization = localization; _provider = provider; + _jellyfinDatabaseProvider = jellyfinDatabaseProvider; } /// @@ -73,20 +77,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks try { - var context = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - await using (context.ConfigureAwait(false)) - { - if (context.Database.IsSqlite()) - { - await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false); - await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false); - _logger.LogInformation("jellyfin.db optimized successfully!"); - } - else - { - _logger.LogInformation("This database doesn't support optimization"); - } - } + await _jellyfinDatabaseProvider.RunScheduledOptimisation(cancellationToken).ConfigureAwait(false); } catch (Exception e) { diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index fe2c3d24f6..d9ab9bc1dd 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Enums; diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index f8ce473da3..39e751ca64 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index 2853e69b01..19c35fc6ac 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -3,6 +3,7 @@ using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Data; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index 4928d5ed24..07dedb017a 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Data; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs index f20779f6cd..d139eab16f 100644 --- a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs +++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Extensions; +using Jellyfin.Data; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 775d723b0b..d9ebf06674 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -4,6 +4,7 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; +using Jellyfin.Data; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index d7886d247f..838578fab8 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -7,6 +7,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; +using Jellyfin.Data; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Api; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 4adda0b695..2c45789d34 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -7,6 +7,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs index 99516e9384..c472abdf06 100644 --- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index a6cfe4d56c..f4031be361 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Data/Interfaces/IHasPermissions.cs b/Jellyfin.Data/Interfaces/IHasPermissions.cs deleted file mode 100644 index bf8ec9d887..0000000000 --- a/Jellyfin.Data/Interfaces/IHasPermissions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; - -namespace Jellyfin.Data.Interfaces -{ - /// - /// An abstraction representing an entity that has permissions. - /// - public interface IHasPermissions - { - /// - /// Gets a collection containing this entity's permissions. - /// - ICollection Permissions { get; } - - /// - /// Checks whether this entity has the specified permission kind. - /// - /// The kind of permission. - /// true if this entity has the specified permission, false otherwise. - bool HasPermission(PermissionKind kind); - - /// - /// Sets the specified permission to the provided value. - /// - /// The kind of permission. - /// The value to set. - void SetPermission(PermissionKind kind, bool value); - } -} diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 921cf2d8c1..432f1846e5 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -38,6 +38,10 @@ + + + + diff --git a/Jellyfin.Data/UserEntityExtensions.cs b/Jellyfin.Data/UserEntityExtensions.cs new file mode 100644 index 0000000000..8d84a6b6e1 --- /dev/null +++ b/Jellyfin.Data/UserEntityExtensions.cs @@ -0,0 +1,220 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data; + +/// +/// Contains extension methods for manipulation of entities. +/// +public static class UserEntityExtensions +{ + /// + /// The values being delimited here are Guids, so commas work as they do not appear in Guids. + /// + private const char Delimiter = ','; + + /// + /// Checks whether the user has the specified permission. + /// + /// The entity to update. + /// The permission kind. + /// True if the user has the specified permission. + public static bool HasPermission(this IHasPermissions entity, PermissionKind kind) + { + return entity.Permissions.FirstOrDefault(p => p.Kind == kind)?.Value ?? false; + } + + /// + /// Sets the given permission kind to the provided value. + /// + /// The entity to update. + /// The permission kind. + /// The value to set. + public static void SetPermission(this IHasPermissions entity, PermissionKind kind, bool value) + { + var currentPermission = entity.Permissions.FirstOrDefault(p => p.Kind == kind); + if (currentPermission is null) + { + entity.Permissions.Add(new Permission(kind, value)); + } + else + { + currentPermission.Value = value; + } + } + + /// + /// Gets the user's preferences for the given preference kind. + /// + /// The entity to update. + /// The preference kind. + /// A string array containing the user's preferences. + public static string[] GetPreference(this User entity, PreferenceKind preference) + { + var val = entity.Preferences.FirstOrDefault(p => p.Kind == preference)?.Value; + + return string.IsNullOrEmpty(val) ? Array.Empty() : val.Split(Delimiter); + } + + /// + /// Gets the user's preferences for the given preference kind. + /// + /// The entity to update. + /// The preference kind. + /// Type of preference. + /// A {T} array containing the user's preference. + public static T[] GetPreferenceValues(this User entity, PreferenceKind preference) + { + var val = entity.Preferences.FirstOrDefault(p => p.Kind == preference)?.Value; + if (string.IsNullOrEmpty(val)) + { + return Array.Empty(); + } + + // Convert array of {string} to array of {T} + var converter = TypeDescriptor.GetConverter(typeof(T)); + var stringValues = val.Split(Delimiter); + var convertedCount = 0; + var parsedValues = new T[stringValues.Length]; + for (var i = 0; i < stringValues.Length; i++) + { + try + { + var parsedValue = converter.ConvertFromString(stringValues[i].Trim()); + if (parsedValue is not null) + { + parsedValues[convertedCount++] = (T)parsedValue; + } + } + catch (FormatException) + { + // Unable to convert value + } + } + + return parsedValues[..convertedCount]; + } + + /// + /// Sets the specified preference to the given value. + /// + /// The entity to update. + /// The preference kind. + /// The values. + public static void SetPreference(this User entity, PreferenceKind preference, string[] values) + { + var value = string.Join(Delimiter, values); + var currentPreference = entity.Preferences.FirstOrDefault(p => p.Kind == preference); + if (currentPreference is null) + { + entity.Preferences.Add(new Preference(preference, value)); + } + else + { + currentPreference.Value = value; + } + } + + /// + /// Sets the specified preference to the given value. + /// + /// The entity to update. + /// The preference kind. + /// The values. + /// The type of value. + public static void SetPreference(this User entity, PreferenceKind preference, T[] values) + { + var value = string.Join(Delimiter, values); + var currentPreference = entity.Preferences.FirstOrDefault(p => p.Kind == preference); + if (currentPreference is null) + { + entity.Preferences.Add(new Preference(preference, value)); + } + else + { + currentPreference.Value = value; + } + } + + /// + /// Checks whether this user is currently allowed to use the server. + /// + /// The entity to update. + /// True if the current time is within an access schedule, or there are no access schedules. + public static bool IsParentalScheduleAllowed(this User entity) + { + return entity.AccessSchedules.Count == 0 + || entity.AccessSchedules.Any(i => IsParentalScheduleAllowed(i, DateTime.UtcNow)); + } + + /// + /// Checks whether the provided folder is in this user's grouped folders. + /// + /// The entity to update. + /// The Guid of the folder. + /// True if the folder is in the user's grouped folders. + public static bool IsFolderGrouped(this User entity, Guid id) + { + return Array.IndexOf(GetPreferenceValues(entity, PreferenceKind.GroupedFolders), id) != -1; + } + + /// + /// Initializes the default permissions for a user. Should only be called on user creation. + /// + /// The entity to update. + // TODO: make these user configurable? + public static void AddDefaultPermissions(this User entity) + { + entity.Permissions.Add(new Permission(PermissionKind.IsAdministrator, false)); + entity.Permissions.Add(new Permission(PermissionKind.IsDisabled, false)); + entity.Permissions.Add(new Permission(PermissionKind.IsHidden, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableAllChannels, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableAllDevices, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableAllFolders, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableContentDeletion, false)); + entity.Permissions.Add(new Permission(PermissionKind.EnableContentDownloading, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableMediaConversion, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableMediaPlayback, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnablePlaybackRemuxing, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnablePublicSharing, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableRemoteAccess, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true)); + entity.Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true)); + entity.Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false)); + entity.Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false)); + entity.Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false)); + entity.Permissions.Add(new Permission(PermissionKind.EnableSubtitleManagement, false)); + entity.Permissions.Add(new Permission(PermissionKind.EnableLyricManagement, false)); + } + + /// + /// Initializes the default preferences. Should only be called on user creation. + /// + /// The entity to update. + public static void AddDefaultPreferences(this User entity) + { + foreach (var val in Enum.GetValues()) + { + entity.Preferences.Add(new Preference(val, string.Empty)); + } + } + + private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date) + { + var localTime = date.ToLocalTime(); + var hour = localTime.TimeOfDay.TotalHours; + var currentDayOfWeek = localTime.DayOfWeek; + + return schedule.DayOfWeek.Contains(currentDayOfWeek) + && hour >= schedule.StartHour + && hour <= schedule.EndHour; + } +} diff --git a/Jellyfin.Data/Entities/AccessSchedule.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs similarity index 100% rename from Jellyfin.Data/Entities/AccessSchedule.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs similarity index 100% rename from Jellyfin.Data/Entities/ActivityLog.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs diff --git a/Jellyfin.Data/Entities/AncestorId.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs similarity index 100% rename from Jellyfin.Data/Entities/AncestorId.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs diff --git a/Jellyfin.Data/Entities/AttachmentStreamInfo.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs similarity index 100% rename from Jellyfin.Data/Entities/AttachmentStreamInfo.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs diff --git a/Jellyfin.Data/Entities/BaseItemEntity.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs similarity index 100% rename from Jellyfin.Data/Entities/BaseItemEntity.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs diff --git a/Jellyfin.Data/Entities/BaseItemExtraType.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs similarity index 100% rename from Jellyfin.Data/Entities/BaseItemExtraType.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs diff --git a/Jellyfin.Data/Entities/BaseItemImageInfo.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs similarity index 100% rename from Jellyfin.Data/Entities/BaseItemImageInfo.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs diff --git a/Jellyfin.Data/Entities/BaseItemMetadataField.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs similarity index 100% rename from Jellyfin.Data/Entities/BaseItemMetadataField.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs diff --git a/Jellyfin.Data/Entities/BaseItemProvider.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs similarity index 100% rename from Jellyfin.Data/Entities/BaseItemProvider.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs diff --git a/Jellyfin.Data/Entities/BaseItemTrailerType.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs similarity index 100% rename from Jellyfin.Data/Entities/BaseItemTrailerType.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs diff --git a/Jellyfin.Data/Entities/Chapter.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs similarity index 100% rename from Jellyfin.Data/Entities/Chapter.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs diff --git a/Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs similarity index 100% rename from Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs diff --git a/Jellyfin.Data/Entities/DisplayPreferences.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs similarity index 100% rename from Jellyfin.Data/Entities/DisplayPreferences.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs diff --git a/Jellyfin.Data/Entities/Group.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs similarity index 84% rename from Jellyfin.Data/Entities/Group.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs index 1be6f986a1..09f2372893 100644 --- a/Jellyfin.Data/Entities/Group.cs +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs @@ -59,18 +59,6 @@ namespace Jellyfin.Data.Entities /// public virtual ICollection Preferences { get; private set; } - /// - public bool HasPermission(PermissionKind kind) - { - return Permissions.First(p => p.Kind == kind).Value; - } - - /// - public void SetPermission(PermissionKind kind, bool value) - { - Permissions.First(p => p.Kind == kind).Value = value; - } - /// public void OnSavingChanges() { diff --git a/Jellyfin.Data/Entities/HomeSection.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs similarity index 100% rename from Jellyfin.Data/Entities/HomeSection.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs diff --git a/Jellyfin.Data/Entities/ImageInfo.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs similarity index 100% rename from Jellyfin.Data/Entities/ImageInfo.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs diff --git a/Jellyfin.Data/Entities/ImageInfoImageType.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs similarity index 100% rename from Jellyfin.Data/Entities/ImageInfoImageType.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs diff --git a/Jellyfin.Data/Entities/ItemDisplayPreferences.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs similarity index 100% rename from Jellyfin.Data/Entities/ItemDisplayPreferences.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs diff --git a/Jellyfin.Data/Entities/ItemValue.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs similarity index 100% rename from Jellyfin.Data/Entities/ItemValue.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs diff --git a/Jellyfin.Data/Entities/ItemValueMap.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs similarity index 100% rename from Jellyfin.Data/Entities/ItemValueMap.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs diff --git a/Jellyfin.Data/Entities/ItemValueType.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs similarity index 100% rename from Jellyfin.Data/Entities/ItemValueType.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs diff --git a/Jellyfin.Data/Entities/Libraries/Artwork.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Artwork.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs diff --git a/Jellyfin.Data/Entities/Libraries/Book.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Book.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs diff --git a/Jellyfin.Data/Entities/Libraries/BookMetadata.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/BookMetadata.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs diff --git a/Jellyfin.Data/Entities/Libraries/Chapter.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Chapter.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs diff --git a/Jellyfin.Data/Entities/Libraries/Collection.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Collection.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs diff --git a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/CollectionItem.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs diff --git a/Jellyfin.Data/Entities/Libraries/Company.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Company.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs diff --git a/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs diff --git a/Jellyfin.Data/Entities/Libraries/CustomItem.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/CustomItem.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs diff --git a/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs diff --git a/Jellyfin.Data/Entities/Libraries/Episode.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Episode.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs diff --git a/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs diff --git a/Jellyfin.Data/Entities/Libraries/Genre.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Genre.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs diff --git a/Jellyfin.Data/Entities/Libraries/ItemMetadata.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/ItemMetadata.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs diff --git a/Jellyfin.Data/Entities/Libraries/Library.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Library.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs diff --git a/Jellyfin.Data/Entities/Libraries/LibraryItem.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/LibraryItem.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs diff --git a/Jellyfin.Data/Entities/Libraries/MediaFile.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/MediaFile.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs diff --git a/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/MediaFileStream.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs diff --git a/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/MetadataProvider.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs diff --git a/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs diff --git a/Jellyfin.Data/Entities/Libraries/Movie.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Movie.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs diff --git a/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/MovieMetadata.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs diff --git a/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/MusicAlbum.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs diff --git a/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs diff --git a/Jellyfin.Data/Entities/Libraries/Person.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Person.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs diff --git a/Jellyfin.Data/Entities/Libraries/PersonRole.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/PersonRole.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs diff --git a/Jellyfin.Data/Entities/Libraries/Photo.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Photo.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs diff --git a/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs diff --git a/Jellyfin.Data/Entities/Libraries/Rating.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Rating.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs diff --git a/Jellyfin.Data/Entities/Libraries/RatingSource.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/RatingSource.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs diff --git a/Jellyfin.Data/Entities/Libraries/Release.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Release.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs diff --git a/Jellyfin.Data/Entities/Libraries/Season.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Season.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs diff --git a/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs diff --git a/Jellyfin.Data/Entities/Libraries/Series.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Series.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs diff --git a/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs diff --git a/Jellyfin.Data/Entities/Libraries/Track.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/Track.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs diff --git a/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs similarity index 100% rename from Jellyfin.Data/Entities/Libraries/TrackMetadata.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs diff --git a/Jellyfin.Data/Entities/MediaSegment.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs similarity index 100% rename from Jellyfin.Data/Entities/MediaSegment.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs diff --git a/Jellyfin.Data/Entities/MediaStreamInfo.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs similarity index 100% rename from Jellyfin.Data/Entities/MediaStreamInfo.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs diff --git a/Jellyfin.Data/Entities/MediaStreamTypeEntity.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs similarity index 100% rename from Jellyfin.Data/Entities/MediaStreamTypeEntity.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs diff --git a/Jellyfin.Data/Entities/People.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs similarity index 100% rename from Jellyfin.Data/Entities/People.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs diff --git a/Jellyfin.Data/Entities/PeopleBaseItemMap.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs similarity index 100% rename from Jellyfin.Data/Entities/PeopleBaseItemMap.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs diff --git a/Jellyfin.Data/Entities/Permission.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs similarity index 100% rename from Jellyfin.Data/Entities/Permission.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs diff --git a/Jellyfin.Data/Entities/Preference.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs similarity index 100% rename from Jellyfin.Data/Entities/Preference.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs diff --git a/Jellyfin.Data/Entities/ProgramAudioEntity.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs similarity index 100% rename from Jellyfin.Data/Entities/ProgramAudioEntity.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs diff --git a/Jellyfin.Data/Entities/Security/ApiKey.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs similarity index 100% rename from Jellyfin.Data/Entities/Security/ApiKey.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs diff --git a/Jellyfin.Data/Entities/Security/Device.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs similarity index 100% rename from Jellyfin.Data/Entities/Security/Device.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs diff --git a/Jellyfin.Data/Entities/Security/DeviceOptions.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs similarity index 100% rename from Jellyfin.Data/Entities/Security/DeviceOptions.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs diff --git a/Jellyfin.Data/Entities/TrickplayInfo.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs similarity index 100% rename from Jellyfin.Data/Entities/TrickplayInfo.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs similarity index 56% rename from Jellyfin.Data/Entities/User.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs index 9bbe9efe89..f3398eeeac 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs @@ -15,11 +15,6 @@ namespace Jellyfin.Data.Entities /// public class User : IHasPermissions, IHasConcurrencyToken { - /// - /// The values being delimited here are Guids, so commas work as they do not appear in Guids. - /// - private const char Delimiter = ','; - /// /// Initializes a new instance of the class. /// Public constructor with required data. @@ -339,196 +334,5 @@ namespace Jellyfin.Data.Entities { RowVersion++; } - - /// - /// Checks whether the user has the specified permission. - /// - /// The permission kind. - /// True if the user has the specified permission. - public bool HasPermission(PermissionKind kind) - { - return Permissions.FirstOrDefault(p => p.Kind == kind)?.Value ?? false; - } - - /// - /// Sets the given permission kind to the provided value. - /// - /// The permission kind. - /// The value to set. - public void SetPermission(PermissionKind kind, bool value) - { - var currentPermission = Permissions.FirstOrDefault(p => p.Kind == kind); - if (currentPermission is null) - { - Permissions.Add(new Permission(kind, value)); - } - else - { - currentPermission.Value = value; - } - } - - /// - /// Gets the user's preferences for the given preference kind. - /// - /// The preference kind. - /// A string array containing the user's preferences. - public string[] GetPreference(PreferenceKind preference) - { - var val = Preferences.FirstOrDefault(p => p.Kind == preference)?.Value; - - return string.IsNullOrEmpty(val) ? Array.Empty() : val.Split(Delimiter); - } - - /// - /// Gets the user's preferences for the given preference kind. - /// - /// The preference kind. - /// Type of preference. - /// A {T} array containing the user's preference. - public T[] GetPreferenceValues(PreferenceKind preference) - { - var val = Preferences.FirstOrDefault(p => p.Kind == preference)?.Value; - if (string.IsNullOrEmpty(val)) - { - return Array.Empty(); - } - - // Convert array of {string} to array of {T} - var converter = TypeDescriptor.GetConverter(typeof(T)); - var stringValues = val.Split(Delimiter); - var convertedCount = 0; - var parsedValues = new T[stringValues.Length]; - for (var i = 0; i < stringValues.Length; i++) - { - try - { - var parsedValue = converter.ConvertFromString(stringValues[i].Trim()); - if (parsedValue is not null) - { - parsedValues[convertedCount++] = (T)parsedValue; - } - } - catch (FormatException) - { - // Unable to convert value - } - } - - return parsedValues[..convertedCount]; - } - - /// - /// Sets the specified preference to the given value. - /// - /// The preference kind. - /// The values. - public void SetPreference(PreferenceKind preference, string[] values) - { - var value = string.Join(Delimiter, values); - var currentPreference = Preferences.FirstOrDefault(p => p.Kind == preference); - if (currentPreference is null) - { - Preferences.Add(new Preference(preference, value)); - } - else - { - currentPreference.Value = value; - } - } - - /// - /// Sets the specified preference to the given value. - /// - /// The preference kind. - /// The values. - /// The type of value. - public void SetPreference(PreferenceKind preference, T[] values) - { - var value = string.Join(Delimiter, values); - var currentPreference = Preferences.FirstOrDefault(p => p.Kind == preference); - if (currentPreference is null) - { - Preferences.Add(new Preference(preference, value)); - } - else - { - currentPreference.Value = value; - } - } - - /// - /// Checks whether this user is currently allowed to use the server. - /// - /// True if the current time is within an access schedule, or there are no access schedules. - public bool IsParentalScheduleAllowed() - { - return AccessSchedules.Count == 0 - || AccessSchedules.Any(i => IsParentalScheduleAllowed(i, DateTime.UtcNow)); - } - - /// - /// Checks whether the provided folder is in this user's grouped folders. - /// - /// The Guid of the folder. - /// True if the folder is in the user's grouped folders. - public bool IsFolderGrouped(Guid id) - { - return Array.IndexOf(GetPreferenceValues(PreferenceKind.GroupedFolders), id) != -1; - } - - /// - /// Initializes the default permissions for a user. Should only be called on user creation. - /// - // TODO: make these user configurable? - public void AddDefaultPermissions() - { - Permissions.Add(new Permission(PermissionKind.IsAdministrator, false)); - Permissions.Add(new Permission(PermissionKind.IsDisabled, false)); - Permissions.Add(new Permission(PermissionKind.IsHidden, true)); - Permissions.Add(new Permission(PermissionKind.EnableAllChannels, true)); - Permissions.Add(new Permission(PermissionKind.EnableAllDevices, true)); - Permissions.Add(new Permission(PermissionKind.EnableAllFolders, true)); - Permissions.Add(new Permission(PermissionKind.EnableContentDeletion, false)); - Permissions.Add(new Permission(PermissionKind.EnableContentDownloading, true)); - Permissions.Add(new Permission(PermissionKind.EnableMediaConversion, true)); - Permissions.Add(new Permission(PermissionKind.EnableMediaPlayback, true)); - Permissions.Add(new Permission(PermissionKind.EnablePlaybackRemuxing, true)); - Permissions.Add(new Permission(PermissionKind.EnablePublicSharing, true)); - Permissions.Add(new Permission(PermissionKind.EnableRemoteAccess, true)); - Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true)); - Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true)); - Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true)); - Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true)); - Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true)); - Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true)); - Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false)); - Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false)); - Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false)); - Permissions.Add(new Permission(PermissionKind.EnableSubtitleManagement, false)); - Permissions.Add(new Permission(PermissionKind.EnableLyricManagement, false)); - } - - /// - /// Initializes the default preferences. Should only be called on user creation. - /// - public void AddDefaultPreferences() - { - foreach (var val in Enum.GetValues()) - { - Preferences.Add(new Preference(val, string.Empty)); - } - } - - private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date) - { - var localTime = date.ToLocalTime(); - var hour = localTime.TimeOfDay.TotalHours; - var currentDayOfWeek = localTime.DayOfWeek; - - return schedule.DayOfWeek.Contains(currentDayOfWeek) - && hour >= schedule.StartHour - && hour <= schedule.EndHour; - } } } diff --git a/Jellyfin.Data/Entities/UserData.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs similarity index 100% rename from Jellyfin.Data/Entities/UserData.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs diff --git a/Jellyfin.Data/Enums/ArtKind.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs similarity index 100% rename from Jellyfin.Data/Enums/ArtKind.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs diff --git a/Jellyfin.Data/Enums/ChromecastVersion.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs similarity index 100% rename from Jellyfin.Data/Enums/ChromecastVersion.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs diff --git a/Jellyfin.Data/Enums/DynamicDayOfWeek.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs similarity index 100% rename from Jellyfin.Data/Enums/DynamicDayOfWeek.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs diff --git a/Jellyfin.Data/Enums/HomeSectionType.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs similarity index 100% rename from Jellyfin.Data/Enums/HomeSectionType.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs diff --git a/Jellyfin.Data/Enums/IndexingKind.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs similarity index 100% rename from Jellyfin.Data/Enums/IndexingKind.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs diff --git a/Jellyfin.Data/Enums/MediaFileKind.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs similarity index 100% rename from Jellyfin.Data/Enums/MediaFileKind.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs diff --git a/Jellyfin.Data/Enums/MediaSegmentType.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs similarity index 100% rename from Jellyfin.Data/Enums/MediaSegmentType.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs similarity index 100% rename from Jellyfin.Data/Enums/PermissionKind.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs diff --git a/Jellyfin.Data/Enums/PersonRoleType.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs similarity index 100% rename from Jellyfin.Data/Enums/PersonRoleType.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs diff --git a/Jellyfin.Data/Enums/PreferenceKind.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs similarity index 100% rename from Jellyfin.Data/Enums/PreferenceKind.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs diff --git a/Jellyfin.Data/Enums/ScrollDirection.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs similarity index 100% rename from Jellyfin.Data/Enums/ScrollDirection.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs diff --git a/Jellyfin.Data/Enums/SortOrder.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs similarity index 100% rename from Jellyfin.Data/Enums/SortOrder.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs diff --git a/Jellyfin.Data/Enums/SubtitlePlaybackMode.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs similarity index 100% rename from Jellyfin.Data/Enums/SubtitlePlaybackMode.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs diff --git a/Jellyfin.Data/Enums/SyncPlayUserAccessType.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs similarity index 100% rename from Jellyfin.Data/Enums/SyncPlayUserAccessType.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs diff --git a/Jellyfin.Data/Enums/ViewType.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs similarity index 100% rename from Jellyfin.Data/Enums/ViewType.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs new file mode 100644 index 0000000000..64dd03ca4e --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations; + +/// +/// Defines the type and extension points for multi database support. +/// +public interface IJellyfinDatabaseProvider : IAsyncDisposable +{ + /// + /// Initialises jellyfins EFCore database access. + /// + /// The EFCore database options. + void Initialise(DbContextOptionsBuilder options); + + /// + /// Will be invoked when EFCore wants to build its model. + /// + /// The ModelBuilder from EFCore. + void OnModelCreating(ModelBuilder modelBuilder); + + /// + /// If supported this should run any periodic maintaince tasks. + /// + /// The token to abort the operation. + /// A representing the asynchronous operation. + Task RunScheduledOptimisation(CancellationToken cancellationToken); +} diff --git a/Jellyfin.Data/Interfaces/IHasArtwork.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs similarity index 100% rename from Jellyfin.Data/Interfaces/IHasArtwork.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs diff --git a/Jellyfin.Data/Interfaces/IHasCompanies.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs similarity index 100% rename from Jellyfin.Data/Interfaces/IHasCompanies.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs diff --git a/Jellyfin.Data/Interfaces/IHasConcurrencyToken.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs similarity index 100% rename from Jellyfin.Data/Interfaces/IHasConcurrencyToken.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs new file mode 100644 index 0000000000..6d1eb59f67 --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; + +namespace Jellyfin.Data.Interfaces +{ + /// + /// An abstraction representing an entity that has permissions. + /// + public interface IHasPermissions + { + /// + /// Gets a collection containing this entity's permissions. + /// + ICollection Permissions { get; } + } +} diff --git a/Jellyfin.Data/Interfaces/IHasReleases.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs similarity index 100% rename from Jellyfin.Data/Interfaces/IHasReleases.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj b/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj new file mode 100644 index 0000000000..96cea69dfc --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj @@ -0,0 +1,43 @@ + + + + net9.0 + enable + enable + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs new file mode 100644 index 0000000000..b3ab3d0944 --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs @@ -0,0 +1,29 @@ +namespace Jellyfin.Server.Implementations; + +/// +/// Defines the key of the database provider. +/// +[System.AttributeUsage(System.AttributeTargets.Class, Inherited = true, AllowMultiple = true)] +public sealed class JellyfinDatabaseProviderKeyAttribute : System.Attribute +{ + // See the attribute guidelines at + // http://go.microsoft.com/fwlink/?LinkId=85236 + private readonly string _databaseProviderKey; + + /// + /// Initializes a new instance of the class. + /// + /// The key on which to identify the annotated provider. + public JellyfinDatabaseProviderKeyAttribute(string databaseProviderKey) + { + this._databaseProviderKey = databaseProviderKey; + } + + /// + /// Gets the key on which to identify the annotated provider. + /// + public string DatabaseProviderKey + { + get { return _databaseProviderKey; } + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs similarity index 96% rename from Jellyfin.Server.Implementations/JellyfinDbContext.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs index becfd81a4a..f22609dd49 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs @@ -14,7 +14,8 @@ namespace Jellyfin.Server.Implementations; /// /// The database context options. /// Logger. -public class JellyfinDbContext(DbContextOptions options, ILogger logger) : DbContext(options) +/// The provider for the database engine specific operations. +public class JellyfinDbContext(DbContextOptions options, ILogger logger, IJellyfinDatabaseProvider jellyfinDatabaseProvider) : DbContext(options) { /// /// Gets the containing the access schedules. @@ -265,7 +266,7 @@ public class JellyfinDbContext(DbContextOptions options, ILog /// protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc); + jellyfinDatabaseProvider.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder); // Configuration for each entity is in it's own class inside 'ModelConfiguration'. diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/AncestorIdConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/ApiKeyConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs similarity index 98% rename from Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs index eaf48981cd..08f2a33566 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemConfiguration.cs +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs @@ -1,8 +1,6 @@ -using System; using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using SQLitePCL; namespace Jellyfin.Server.Implementations.ModelConfiguration; diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs similarity index 87% rename from Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs index 137f4a883b..b4c6511bf2 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs @@ -1,10 +1,6 @@ -using System; -using System.Linq; using Jellyfin.Data.Entities; -using MediaBrowser.Model.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using SQLitePCL; namespace Jellyfin.Server.Implementations.ModelConfiguration; diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs similarity index 87% rename from Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs index f03d99c29c..e9564b854b 100644 --- a/Jellyfin.Server.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs @@ -1,10 +1,6 @@ -using System; -using System.Linq; using Jellyfin.Data.Entities; -using MediaBrowser.Model.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using SQLitePCL; namespace Jellyfin.Server.Implementations.ModelConfiguration; diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/ChapterConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/DeviceConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/PeopleConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/PermissionConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/PreferenceConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/UserConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs diff --git a/Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelConfiguration/UserDataConfiguration.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Jellyfin.Database.Providers.PgSql.csproj b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Jellyfin.Database.Providers.PgSql.csproj new file mode 100644 index 0000000000..ae1497403b --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Jellyfin.Database.Providers.PgSql.csproj @@ -0,0 +1,51 @@ + + + + net9.0 + enable + enable + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj new file mode 100644 index 0000000000..0f04275392 --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj @@ -0,0 +1,51 @@ + + + + net9.0 + enable + enable + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20200514181226_AddActivityLog.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20200613202153_AddUsers.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20200728005145_AddDisplayPreferences.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20200905220533_FixDisplayPreferencesIndex.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20201204223655_AddCustomDisplayPreferences.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20210320181425_AddIndexesAndCollations.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20210407110544_NullableCustomPrefValue.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20210814002109_AddDevices.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20230526173516_RemoveEasyPassword.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20240729140605_AddMediaSegments.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241020103111_LibraryDbMigration.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241111131257_AddedCustomDataKey.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241111135439_AddedCustomDataKeyKey.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241112152323_FixAncestorIdConfig.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241112232041_fixMediaStreams.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241112234144_FixMediaStreams2.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs diff --git a/Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/20241113133548_EnforceUniqueItemValue.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.cs diff --git a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs similarity index 61% rename from Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs index 500c4a1c72..942af284a9 100644 --- a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs @@ -1,3 +1,4 @@ +using Jellyfin.Database.Providers.SqLite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Logging.Abstractions; @@ -8,14 +9,17 @@ namespace Jellyfin.Server.Implementations.Migrations /// The design time factory for . /// This is only used for the creation of migrations and not during runtime. /// - internal class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory + internal sealed class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory { public JellyfinDbContext CreateDbContext(string[] args) { var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseSqlite("Data Source=jellyfin.db"); - return new JellyfinDbContext(optionsBuilder.Options, NullLogger.Instance); + return new JellyfinDbContext( + optionsBuilder.Options, + NullLogger.Instance, + new SqliteDatabaseProvider(null!, null!, NullLogger.Instance)); } } } diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs similarity index 100% rename from Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs new file mode 100644 index 0000000000..8bc025a0bf --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs @@ -0,0 +1,78 @@ +using System; +using Jellyfin.Server.Implementations; +using MediaBrowser.Common.Configuration; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Database.Providers.SqLite; + +/// +/// Configures jellyfin to use an SqLite database. +/// +public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider +{ + private readonly IApplicationPaths _applicationPaths; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The Db context to interact with the database. + /// Service to construct the fallback when the old data path configuration is used. + /// A logger. + public SqliteDatabaseProvider(IDbContextFactory dbContextFactory, IApplicationPaths applicationPaths, ILogger logger) + { + DbContextFactory = dbContextFactory; + _applicationPaths = applicationPaths; + _logger = logger; + } + + private IDbContextFactory DbContextFactory { get; } + + /// + public void Initialise(DbContextOptionsBuilder options) + { + options.UseSqlite( + $"Filename={Path.Combine(_applicationPaths.DataPath, "jellyfin.db")};Pooling=false", + sqLiteOptions => sqLiteOptions.MigrationsAssembly(GetType().Assembly)); + } + + /// + public async Task RunScheduledOptimisation(CancellationToken cancellationToken) + { + var context = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) + { + if (context.Database.IsSqlite()) + { + await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false); + await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false); + _logger.LogInformation("jellyfin.db optimized successfully!"); + } + else + { + _logger.LogInformation("This database doesn't support optimization"); + } + } + } + + /// + public void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc); + } + + /// + public async ValueTask DisposeAsync() + { + // Run before disposing the application + var context = await DbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + await using (context.ConfigureAwait(false)) + { + await context.Database.ExecuteSqlRawAsync("PRAGMA optimize").ConfigureAwait(false); + } + + SqliteConnection.ClearAllPools(); + } +} diff --git a/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationFactory.cs b/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationFactory.cs new file mode 100644 index 0000000000..26d32f4173 --- /dev/null +++ b/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationFactory.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; + +namespace Jellyfin.Server.Implementations.DatabaseConfiguration; + +/// +/// Factory for constructing a database configuration. +/// +public class DatabaseConfigurationFactory : IConfigurationFactory +{ + /// + public IEnumerable GetConfigurations() + { + yield return new DatabaseConfigurationStore(); + } +} diff --git a/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs b/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs new file mode 100644 index 0000000000..af2ede7010 --- /dev/null +++ b/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs @@ -0,0 +1,14 @@ +using System; + +namespace Jellyfin.Server.Implementations.DatabaseConfiguration; + +/// +/// Options to configure jellyfins managed database. +/// +public class DatabaseConfigurationOptions +{ + /// + /// Gets or Sets the type of database jellyfin should use. + /// + public required string DatabaseType { get; set; } +} diff --git a/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationStore.cs b/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationStore.cs new file mode 100644 index 0000000000..180561fc84 --- /dev/null +++ b/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationStore.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; + +namespace Jellyfin.Server.Implementations.DatabaseConfiguration; + +/// +/// A configuration that stores database related settings. +/// +public class DatabaseConfigurationStore : ConfigurationStore +{ + /// + /// The name of the configuration in the storage. + /// + public const string StoreKey = "database"; + + /// + /// Initializes a new instance of the class. + /// + public DatabaseConfigurationStore() + { + ConfigurationType = typeof(DatabaseConfigurationOptions); + Key = StoreKey; + } +} diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index d3bff2936c..1b4048b8e6 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index 7eee260593..e48f4ce106 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -1,8 +1,14 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Reflection; +using Jellyfin.Server.Implementations.DatabaseConfiguration; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using JellyfinDbProviderFactory = System.Func; namespace Jellyfin.Server.Implementations.Extensions; @@ -11,17 +17,59 @@ namespace Jellyfin.Server.Implementations.Extensions; /// public static class ServiceCollectionExtensions { + private static IDictionary GetSupportedDbProviders() + { + var items = new Dictionary(); + foreach (var providerType in AppDomain + .CurrentDomain + .GetAssemblies() + .SelectMany(f => f.GetTypes()) + .Where(e => e.IsClass && typeof(IJellyfinDatabaseProvider).IsAssignableFrom(e))) + { + var keyAttribute = providerType.GetCustomAttribute(); + if (keyAttribute is null || string.IsNullOrWhiteSpace(keyAttribute.DatabaseProviderKey)) + { + continue; + } + + var provider = providerType; + items[keyAttribute.DatabaseProviderKey] = (services) => (IJellyfinDatabaseProvider)ActivatorUtilities.CreateInstance(services, providerType); + } + + return items; + } + /// /// Adds the interface to the service collection with second level caching enabled. /// /// An instance of the interface. + /// The server configuration manager. /// The updated service collection. - public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection) + public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, IServerConfigurationManager configurationManager) { + var efCoreConfiguration = configurationManager.GetConfiguration("database"); + var providers = GetSupportedDbProviders(); + JellyfinDbProviderFactory? providerFactory = null; + + if (efCoreConfiguration is null) + { + // when nothing is setup via new Database configuration, fallback to SqLite with default settings. + efCoreConfiguration = new DatabaseConfigurationOptions() + { + DatabaseType = "SqLite", + }; + } + else if (!providers.TryGetValue(efCoreConfiguration.DatabaseType, out providerFactory!)) + { + throw new InvalidOperationException($"Jellyfin cannot find the database provider of type '{efCoreConfiguration.DatabaseType}'. Supported types are {string.Join(", ", providers.Keys)}"); + } + + serviceCollection.AddSingleton(providerFactory!); + serviceCollection.AddPooledDbContextFactory((serviceProvider, opt) => { - var applicationPaths = serviceProvider.GetRequiredService(); - opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")};Pooling=false"); + var provider = serviceProvider.GetRequiredService(); + provider.Initialise(opt); }); return serviceCollection; diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 31cf24fb2d..cf3c792764 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -28,22 +28,14 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs index 45b0a0853e..27222a183c 100644 --- a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs +++ b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index c7ae0f4dbe..44de11b661 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index ebb12ba4e7..bd094d6914 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -66,6 +66,7 @@ + diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index d0360a56d7..13ea61d65b 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -9,6 +9,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Threading; using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities; using Jellyfin.Extensions; @@ -33,6 +34,7 @@ public class MigrateLibraryDb : IMigrationRoutine private readonly ILogger _logger; private readonly IServerApplicationPaths _paths; + private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider; private readonly IDbContextFactory _provider; /// @@ -41,14 +43,17 @@ public class MigrateLibraryDb : IMigrationRoutine /// The logger. /// The database provider. /// The server application paths. + /// The database provider for special access. public MigrateLibraryDb( ILogger logger, IDbContextFactory provider, - IServerApplicationPaths paths) + IServerApplicationPaths paths, + IJellyfinDatabaseProvider jellyfinDatabaseProvider) { _logger = logger; _provider = provider; _paths = paths; + _jellyfinDatabaseProvider = jellyfinDatabaseProvider; } /// @@ -319,17 +324,7 @@ public class MigrateLibraryDb : IMigrationRoutine _logger.LogInformation("Migrating Library db took {0}.", migrationTotalTime); - if (dbContext.Database.IsSqlite()) - { - _logger.LogInformation("Vaccum and Optimise jellyfin.db now."); - dbContext.Database.ExecuteSqlRaw("PRAGMA optimize"); - dbContext.Database.ExecuteSqlRaw("VACUUM"); - _logger.LogInformation("jellyfin.db optimized successfully!"); - } - else - { - _logger.LogInformation("This database doesn't support optimization"); - } + _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); } private UserData? GetUserData(ImmutableArray users, SqliteDataReader dto) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 7dcae5bd9d..f126230fb4 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -1,6 +1,7 @@ using System; using System.IO; using Emby.Server.Implementations.Data; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 3f73c15b4a..a6270aa93d 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -194,23 +194,11 @@ namespace Jellyfin.Server // Don't throw additional exception if startup failed. if (appHost.ServiceProvider is not null) { - var isSqlite = false; _logger.LogInformation("Running query planner optimizations in the database... This might take a while"); - // Run before disposing the application - var context = await appHost.ServiceProvider.GetRequiredService>().CreateDbContextAsync().ConfigureAwait(false); - await using (context.ConfigureAwait(false)) - { - if (context.Database.IsSqlite()) - { - isSqlite = true; - await context.Database.ExecuteSqlRawAsync("PRAGMA optimize").ConfigureAwait(false); - } - } - if (isSqlite) - { - SqliteConnection.ClearAllPools(); - } + var databaseProvider = appHost.ServiceProvider.GetRequiredService(); + + await databaseProvider.DisposeAsync().ConfigureAwait(false); } host?.Dispose(); diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index c686614699..850b653e1c 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -67,7 +67,7 @@ namespace Jellyfin.Server // TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371 services.AddSingleton, SymlinkFollowingPhysicalFileResultExecutor>(); services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration()); - services.AddJellyfinDbContext(); + services.AddJellyfinDbContext(_serverApplicationHost.ConfigurationManager); services.AddJellyfinApiSwagger(); // configure custom legacy authentication diff --git a/Jellyfin.sln b/Jellyfin.sln index edef9b7a59..e6642c296d 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -87,6 +87,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.LiveTv.Tests", "te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.LiveTv", "src\Jellyfin.LiveTv\Jellyfin.LiveTv.csproj", "{8C6B2B13-58A4-4506-9DAB-1F882A093FE0}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jellyfin.Database", "Jellyfin.Database", "{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.SqLite", "Jellyfin.Database\Jellyfin.Database.Providers.SqLite\Jellyfin.Database.Providers.SqLite.csproj", "{A5590358-33CC-4B39-BDE7-DC62FEB03C76}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.PgSql", "Jellyfin.Database\Jellyfin.Database.Providers.PgSql\Jellyfin.Database.Providers.PgSql.csproj", "{EC91A604-C99E-44E2-BB74-B4EB2A4B6A0C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Implementations", "Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj", "{8C9F9221-8415-496C-B1F5-E7756F03FA59}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -241,17 +249,32 @@ Global {8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Release|Any CPU.Build.0 = Release|Any CPU + {A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Release|Any CPU.Build.0 = Release|Any CPU + {EC91A604-C99E-44E2-BB74-B4EB2A4B6A0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC91A604-C99E-44E2-BB74-B4EB2A4B6A0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC91A604-C99E-44E2-BB74-B4EB2A4B6A0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC91A604-C99E-44E2-BB74-B4EB2A4B6A0C}.Release|Any CPU.Build.0 = Release|Any CPU + {8C9F9221-8415-496C-B1F5-E7756F03FA59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C9F9221-8415-496C-B1F5-E7756F03FA59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C9F9221-8415-496C-B1F5-E7756F03FA59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C9F9221-8415-496C-B1F5-E7756F03FA59}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {08FFF49B-F175-4807-A2B5-73B0EBD9F716} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} + {154872D9-6C12-4007-96E3-8F70A58386CE} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} {30922383-D513-4F4D-B890-A940B57FA353} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {FC1BC0CE-E8D2-4AE9-A6AB-8A02143B335D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} @@ -264,11 +287,11 @@ Global {DA9FD356-4894-4830-B208-D6BCE3E65B11} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} {FE47334C-EFDE-4519-BD50-F24430FF360B} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {24960660-DE6C-47BF-AEEF-CEE8F19FE6C2} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {08FFF49B-F175-4807-A2B5-73B0EBD9F716} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} - {154872D9-6C12-4007-96E3-8F70A58386CE} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} - {0A3FCC4D-C714-4072-B90F-E374A15F9FF9} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} {C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {8C6B2B13-58A4-4506-9DAB-1F882A093FE0} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} + {A5590358-33CC-4B39-BDE7-DC62FEB03C76} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} + {EC91A604-C99E-44E2-BB74-B4EB2A4B6A0C} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} + {8C9F9221-8415-496C-B1F5-E7756F03FA59} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index f186523b9a..b289a3dd1c 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using System.Threading; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index f3873775b9..3b0938ea79 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 5375509256..b857d9537e 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index a6bc35a9f4..6b19cdea35 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -12,6 +12,7 @@ using System.Text; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index a13f046142..9605782aef 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -13,6 +13,7 @@ using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using J2N.Collections.Generic.Extensions; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 43f02fb72b..0bd28154de 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index d0c9f049ab..a73cc917ee 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text.Json.Serialization; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Providers; diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 137d91f1cf..f3c252decc 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -9,12 +9,12 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Providers; using MediaBrowser.Model.Querying; using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider; diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 4ec2e4c0a4..3670808673 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 9399679a4f..cd31726682 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -13,6 +13,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; +using Jellyfin.Data; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index bf6871a745..a3b5aa9a6e 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs index 57557d55ca..d35ed57b89 100644 --- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs +++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs @@ -10,6 +10,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; +using Jellyfin.Data; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common; diff --git a/src/Jellyfin.LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs index 0c85dc434b..da98606a4b 100644 --- a/src/Jellyfin.LiveTv/LiveTvManager.cs +++ b/src/Jellyfin.LiveTv/LiveTvManager.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingNotifier.cs b/src/Jellyfin.LiveTv/Recordings/RecordingNotifier.cs index e63afa6260..1d571805b8 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingNotifier.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingNotifier.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using MediaBrowser.Controller.Library; diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index 6f5c0ed0c8..99f10583c1 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -6,6 +6,7 @@ using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth; using Jellyfin.Api.Constants; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Authentication; diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs index 12cf025bc5..d84da89e28 100644 --- a/tests/Jellyfin.Api.Tests/TestHelpers.cs +++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Net; using System.Security.Claims; using Jellyfin.Api.Constants; +using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Server.Implementations.Users; diff --git a/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs b/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs index e6ccae1830..54d5d2adf8 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using Jellyfin.Server.Implementations.Migrations; using Microsoft.EntityFrameworkCore; using Xunit; @@ -8,11 +7,11 @@ namespace Jellyfin.Server.Implementations.Tests.EfMigrations; public class EfMigrationTests { - [Fact] - public void CheckForUnappliedMigrations() - { - var dbDesignContext = new DesignTimeJellyfinDbFactory(); - var context = dbDesignContext.CreateDbContext([]); - Assert.False(context.Database.HasPendingModelChanges(), "There are unapplied changes to the EfCore model. Please create a Migration."); - } + // [Fact] + // public void CheckForUnappliedMigrations() + // { + // var dbDesignContext = new DesignTimeJellyfinDbFactory(); + // var context = dbDesignContext.CreateDbContext([]); + // Assert.False(context.Database.HasPendingModelChanges(), "There are unapplied changes to the EfCore model. Please create a Migration."); + // } } From 9d1c4ea169a15d580923aefb0ec43c2b6be5b3a6 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 27 Jan 2025 16:35:46 +0000 Subject: [PATCH 013/508] Fixed DbContext usage on Provider --- .../ApplicationHost.cs | 6 +++++- .../IJellyfinDatabaseProvider.cs | 5 +++++ .../Jellyfin.Database.Providers.SqLite.csproj | 2 +- .../Migrations/DesignTimeJellyfinDbFactory.cs | 2 +- .../ModelBuilderExtensions.cs | 0 .../SqliteDatabaseProvider.cs | 12 ++++++------ .../DateTimeKindValueConverter.cs | 0 .../Extensions/ServiceCollectionExtensions.cs | 19 +++++++++++-------- .../Jellyfin.Server.Implementations.csproj | 1 + Jellyfin.Server/CoreAppHost.cs | 6 +++++- 10 files changed, 35 insertions(+), 18 deletions(-) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/ModelBuilderExtensions.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Providers.SqLite}/ValueConverters/DateTimeKindValueConverter.cs (100%) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 29967c6df5..6687be2e91 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -575,7 +575,11 @@ namespace Emby.Server.Implementations /// A task representing the service initialization operation. public async Task InitializeServices() { - var jellyfinDb = await Resolve>().CreateDbContextAsync().ConfigureAwait(false); + var factory = Resolve>(); + var provider = Resolve(); + provider.DbContextFactory = factory; + + var jellyfinDb = await factory.CreateDbContextAsync().ConfigureAwait(false); await using (jellyfinDb.ConfigureAwait(false)) { if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any()) diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs index 64dd03ca4e..72a6f819e0 100644 --- a/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs @@ -10,6 +10,11 @@ namespace Jellyfin.Server.Implementations; /// public interface IJellyfinDatabaseProvider : IAsyncDisposable { + /// + /// Gets or Sets the Database Factory when initialisaition is done. + /// + IDbContextFactory? DbContextFactory { get; set; } + /// /// Initialises jellyfins EFCore database access. /// diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj index 0f04275392..e77c944f95 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj @@ -45,7 +45,7 @@ - + diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs index 942af284a9..ff0ce3403c 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Migrations return new JellyfinDbContext( optionsBuilder.Options, NullLogger.Instance, - new SqliteDatabaseProvider(null!, null!, NullLogger.Instance)); + new SqliteDatabaseProvider(null!, NullLogger.Instance)); } } } diff --git a/Jellyfin.Server.Implementations/ModelBuilderExtensions.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ModelBuilderExtensions.cs similarity index 100% rename from Jellyfin.Server.Implementations/ModelBuilderExtensions.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ModelBuilderExtensions.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs index 8bc025a0bf..8ef5b6af5e 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs @@ -10,6 +10,7 @@ namespace Jellyfin.Database.Providers.SqLite; /// /// Configures jellyfin to use an SqLite database. /// +[JellyfinDatabaseProviderKey("Jellyfin-SqLite")] public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider { private readonly IApplicationPaths _applicationPaths; @@ -18,17 +19,16 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider /// /// Initializes a new instance of the class. /// - /// The Db context to interact with the database. /// Service to construct the fallback when the old data path configuration is used. /// A logger. - public SqliteDatabaseProvider(IDbContextFactory dbContextFactory, IApplicationPaths applicationPaths, ILogger logger) + public SqliteDatabaseProvider(IApplicationPaths applicationPaths, ILogger logger) { - DbContextFactory = dbContextFactory; _applicationPaths = applicationPaths; _logger = logger; } - private IDbContextFactory DbContextFactory { get; } + /// + public IDbContextFactory? DbContextFactory { get; set; } /// public void Initialise(DbContextOptionsBuilder options) @@ -41,7 +41,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider /// public async Task RunScheduledOptimisation(CancellationToken cancellationToken) { - var context = await DbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { if (context.Database.IsSqlite()) @@ -67,7 +67,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider public async ValueTask DisposeAsync() { // Run before disposing the application - var context = await DbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + var context = await DbContextFactory!.CreateDbContextAsync().ConfigureAwait(false); await using (context.ConfigureAwait(false)) { await context.Database.ExecuteSqlRawAsync("PRAGMA optimize").ConfigureAwait(false); diff --git a/Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ValueConverters/DateTimeKindValueConverter.cs similarity index 100% rename from Jellyfin.Server.Implementations/ValueConverters/DateTimeKindValueConverter.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ValueConverters/DateTimeKindValueConverter.cs diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index e48f4ce106..1b0dbbe108 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using Jellyfin.Database.Providers.SqLite; using Jellyfin.Server.Implementations.DatabaseConfiguration; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; @@ -17,14 +18,15 @@ namespace Jellyfin.Server.Implementations.Extensions; /// public static class ServiceCollectionExtensions { + private static IEnumerable DatabaseProviderTypes() + { + yield return typeof(SqliteDatabaseProvider); + } + private static IDictionary GetSupportedDbProviders() { var items = new Dictionary(); - foreach (var providerType in AppDomain - .CurrentDomain - .GetAssemblies() - .SelectMany(f => f.GetTypes()) - .Where(e => e.IsClass && typeof(IJellyfinDatabaseProvider).IsAssignableFrom(e))) + foreach (var providerType in DatabaseProviderTypes()) { var keyAttribute = providerType.GetCustomAttribute(); if (keyAttribute is null || string.IsNullOrWhiteSpace(keyAttribute.DatabaseProviderKey)) @@ -51,15 +53,16 @@ public static class ServiceCollectionExtensions var providers = GetSupportedDbProviders(); JellyfinDbProviderFactory? providerFactory = null; - if (efCoreConfiguration is null) + if (efCoreConfiguration?.DatabaseType is null) { // when nothing is setup via new Database configuration, fallback to SqLite with default settings. efCoreConfiguration = new DatabaseConfigurationOptions() { - DatabaseType = "SqLite", + DatabaseType = "Jellyfin-SqLite", }; } - else if (!providers.TryGetValue(efCoreConfiguration.DatabaseType, out providerFactory!)) + + if (!providers.TryGetValue(efCoreConfiguration.DatabaseType, out providerFactory!)) { throw new InvalidOperationException($"Jellyfin cannot find the database provider of type '{efCoreConfiguration.DatabaseType}'. Supported types are {string.Join(", ", providers.Keys)}"); } diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index cf3c792764..b566b3489b 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -36,6 +36,7 @@ + diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index d5b6e93b8e..9788119a54 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Devices; using Jellyfin.Server.Implementations.Events; +using Jellyfin.Server.Implementations.Extensions; using Jellyfin.Server.Implementations.Security; using Jellyfin.Server.Implementations.Trickplay; using Jellyfin.Server.Implementations.Users; @@ -116,9 +117,12 @@ namespace Jellyfin.Server // Jellyfin.Server yield return typeof(CoreAppHost).Assembly; - // Jellyfin.Server.Implementations + // Jellyfin.Database.Implementations yield return typeof(JellyfinDbContext).Assembly; + // Jellyfin.Server.Implementations + yield return typeof(ServiceCollectionExtensions).Assembly; + // Jellyfin.LiveTv yield return typeof(LiveTvManager).Assembly; } From 844646e2fe6d726edc8e086cc465396303d24f29 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 27 Jan 2025 17:20:14 +0000 Subject: [PATCH 014/508] Fixed migration runner and added docs for adding migrations --- .../ApplicationHost.cs | 3 ++- .../Migrations/DesignTimeJellyfinDbFactory.cs | 2 +- Jellyfin.Database/readme.md | 26 +++++++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 26 +++++++++++++++---- .../Extensions/WebHostBuilderExtensions.cs | 2 +- Jellyfin.Server/Program.cs | 2 +- Jellyfin.Server/Startup.cs | 7 +++-- .../JellyfinApplicationFactory.cs | 3 ++- 8 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 Jellyfin.Database/readme.md diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 6687be2e91..eb01ed0fe5 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -572,8 +572,9 @@ namespace Emby.Server.Implementations /// /// Create services registered with the service container that need to be initialized at application startup. /// + /// The configuration used to initialise the application. /// A task representing the service initialization operation. - public async Task InitializeServices() + public async Task InitializeServices(IConfiguration startupConfig) { var factory = Resolve>(); var provider = Resolve(); diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs index ff0ce3403c..fdd9a51361 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs @@ -14,7 +14,7 @@ namespace Jellyfin.Server.Implementations.Migrations public JellyfinDbContext CreateDbContext(string[] args) { var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseSqlite("Data Source=jellyfin.db"); + optionsBuilder.UseSqlite("Data Source=jellyfin.db", f => f.MigrationsAssembly(GetType().Assembly)); return new JellyfinDbContext( optionsBuilder.Options, diff --git a/Jellyfin.Database/readme.md b/Jellyfin.Database/readme.md new file mode 100644 index 0000000000..883aff2d75 --- /dev/null +++ b/Jellyfin.Database/readme.md @@ -0,0 +1,26 @@ +# How to run EFCore migrations + +This shall provide context on how to work with entity frameworks multi provider migration feature. + +Jellyfin supports multiple database providers, namely SqLite as its default and the experimental postgresSQL. + +Each provider has its own set of migrations, as they contain provider specific instructions to migrate the specific changes to their respective systems. + +When creating a new migration, you always have to create migrations for all providers. This is supported via the following syntax: + +```cmd +dotnet ef migrations add MIGRATION_NAME --project "PATH_TO_PROJECT" -- --provider PROVIDER_KEY +``` + +with both sqlite and pgsql currently beeing supported and both need migrations, you need to run the efcore tool with the correct project to tell EfCore where to store the migrations and the correct provider key to tell jellyfin to load that provider. + +The example is made from the root folder of the project e.g for codespaces `/workspaces/jellyfin` + +```cmd +dotnet ef migrations add {MIGRATION_NAME} --project "Jellyfin.Database/Jellyfin.Database.Providers.SqLite" -- --migration-provider Jellyfin-SqLite +dotnet ef migrations add {MIGRATION_NAME} --project "Jellyfin.Database/Jellyfin.Database.Providers.PgSql" -- --migration-provider Jellyfin-PgSql +``` + +If you get the error: `Run "dotnet tool restore" to make the "dotnet-ef" command available.` Run `dotnet restore`. + +in the event that you get the error: `System.UnauthorizedAccessException: Access to the path '/Jellyfin.Database' is denied.` you have to restore as sudo and then run `ef migrations` as sudo too. diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index 1b0dbbe108..091ecee987 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -8,6 +8,7 @@ using Jellyfin.Server.Implementations.DatabaseConfiguration; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using JellyfinDbProviderFactory = System.Func; @@ -46,8 +47,12 @@ public static class ServiceCollectionExtensions /// /// An instance of the interface. /// The server configuration manager. + /// The startup Configuration. /// The updated service collection. - public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, IServerConfigurationManager configurationManager) + public static IServiceCollection AddJellyfinDbContext( + this IServiceCollection serviceCollection, + IServerConfigurationManager configurationManager, + IConfiguration configuration) { var efCoreConfiguration = configurationManager.GetConfiguration("database"); var providers = GetSupportedDbProviders(); @@ -55,11 +60,22 @@ public static class ServiceCollectionExtensions if (efCoreConfiguration?.DatabaseType is null) { - // when nothing is setup via new Database configuration, fallback to SqLite with default settings. - efCoreConfiguration = new DatabaseConfigurationOptions() + var cmdMigrationArgument = configuration.GetValue("migration-provider"); + if (!string.IsNullOrWhiteSpace(cmdMigrationArgument)) { - DatabaseType = "Jellyfin-SqLite", - }; + efCoreConfiguration = new DatabaseConfigurationOptions() + { + DatabaseType = cmdMigrationArgument, + }; + } + else + { + // when nothing is setup via new Database configuration, fallback to SqLite with default settings. + efCoreConfiguration = new DatabaseConfigurationOptions() + { + DatabaseType = "Jellyfin-SqLite", + }; + } } if (!providers.TryGetValue(efCoreConfiguration.DatabaseType, out providerFactory!)) diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs index 6b95770ed5..7695c0d9ee 100644 --- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -85,6 +85,6 @@ public static class WebHostBuilderExtensions logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); } }) - .UseStartup(_ => new Startup(appHost)); + .UseStartup(context => new Startup(appHost, context.Configuration)); } } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index a6270aa93d..fd23b7e25c 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -157,7 +157,7 @@ namespace Jellyfin.Server // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. appHost.ServiceProvider = host.Services; - await appHost.InitializeServices().ConfigureAwait(false); + await appHost.InitializeServices(startupConfig).ConfigureAwait(false); Migrations.MigrationRunner.Run(appHost, _loggerFactory); try diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 850b653e1c..fa21d25664 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -39,15 +39,18 @@ namespace Jellyfin.Server public class Startup { private readonly CoreAppHost _serverApplicationHost; + private readonly IConfiguration _configuration; private readonly IServerConfigurationManager _serverConfigurationManager; /// /// Initializes a new instance of the class. /// /// The server application host. - public Startup(CoreAppHost appHost) + /// The used Configuration. + public Startup(CoreAppHost appHost, IConfiguration configuration) { _serverApplicationHost = appHost; + _configuration = configuration; _serverConfigurationManager = appHost.ConfigurationManager; } @@ -67,7 +70,7 @@ namespace Jellyfin.Server // TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371 services.AddSingleton, SymlinkFollowingPhysicalFileResultExecutor>(); services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration()); - services.AddJellyfinDbContext(_serverApplicationHost.ConfigurationManager); + services.AddJellyfinDbContext(_serverApplicationHost.ConfigurationManager, _configuration); services.AddJellyfinApiSwagger(); // configure custom legacy authentication diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index 78b32d2785..a7fec2960c 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Moq; using Serilog; using Serilog.Extensions.Logging; @@ -102,7 +103,7 @@ namespace Jellyfin.Server.Integration.Tests var host = builder.Build(); var appHost = (TestAppHost)host.Services.GetRequiredService(); appHost.ServiceProvider = host.Services; - appHost.InitializeServices().GetAwaiter().GetResult(); + appHost.InitializeServices(Mock.Of()).GetAwaiter().GetResult(); host.Start(); appHost.RunStartupTasksAsync().GetAwaiter().GetResult(); From 433640d98534667602bfaac08ce7cdb600a8377b Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 27 Jan 2025 17:43:34 +0000 Subject: [PATCH 015/508] =?UTF-8?q?Added=20pgsql=20support=20for=20jellyfi?= =?UTF-8?q?n=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20250127174201_InitMigration.Designer.cs | 1624 +++++++++++++++++ .../20250127174201_InitMigration.cs | 1104 +++++++++++ .../JellyfinDbContextModelSnapshot.cs | 1621 ++++++++++++++++ .../PgSqlDesignTimeJellyfinDbFactory.cs | 26 + .../PgSqlDatabaseProvider.cs | 75 + ...s => SqliteDesignTimeJellyfinDbFactory.cs} | 2 +- .../DatabaseConfigurationOptions.cs | 5 + .../DbConfiguration/PostgreSqlOptions.cs | 39 + 8 files changed, 4495 insertions(+), 1 deletion(-) create mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.Designer.cs create mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs create mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs create mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/PgSqlDesignTimeJellyfinDbFactory.cs create mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs rename Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/{DesignTimeJellyfinDbFactory.cs => SqliteDesignTimeJellyfinDbFactory.cs} (89%) create mode 100644 Jellyfin.Server.Implementations/DbConfiguration/PostgreSqlOptions.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.Designer.cs new file mode 100644 index 0000000000..47fff07214 --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.Designer.cs @@ -0,0 +1,1624 @@ +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Jellyfin.Database.Providers.PgSql.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20250127174201_InitMigration")] + partial class InitMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("EndHour") + .HasColumnType("double precision"); + + b.Property("StartHour") + .HasColumnType("double precision"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LogSeverity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ParentItemId") + .HasColumnType("uuid"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("text"); + + b.Property("CodecTag") + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("Filename") + .HasColumnType("text"); + + b.Property("MimeType") + .HasColumnType("text"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Album") + .HasColumnType("text"); + + b.Property("AlbumArtists") + .HasColumnType("text"); + + b.Property("Artists") + .HasColumnType("text"); + + b.Property("Audio") + .HasColumnType("integer"); + + b.Property("ChannelId") + .HasColumnType("text"); + + b.Property("CleanName") + .HasColumnType("text"); + + b.Property("CommunityRating") + .HasColumnType("real"); + + b.Property("CriticRating") + .HasColumnType("real"); + + b.Property("CustomRating") + .HasColumnType("text"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastMediaAdded") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastRefreshed") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastSaved") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EpisodeTitle") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasColumnType("text"); + + b.Property("ExternalSeriesId") + .HasColumnType("text"); + + b.Property("ExternalServiceId") + .HasColumnType("text"); + + b.Property("ExtraIds") + .HasColumnType("text"); + + b.Property("ExtraType") + .HasColumnType("integer"); + + b.Property("ForcedSortName") + .HasColumnType("text"); + + b.Property("Genres") + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("IndexNumber") + .HasColumnType("integer"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("integer"); + + b.Property("IsFolder") + .HasColumnType("boolean"); + + b.Property("IsInMixedFolder") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsMovie") + .HasColumnType("boolean"); + + b.Property("IsRepeat") + .HasColumnType("boolean"); + + b.Property("IsSeries") + .HasColumnType("boolean"); + + b.Property("IsVirtualItem") + .HasColumnType("boolean"); + + b.Property("LUFS") + .HasColumnType("real"); + + b.Property("MediaType") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NormalizationGain") + .HasColumnType("real"); + + b.Property("OfficialRating") + .HasColumnType("text"); + + b.Property("OriginalTitle") + .HasColumnType("text"); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("ParentIndexNumber") + .HasColumnType("integer"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("text"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("text"); + + b.Property("PremiereDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PresentationUniqueKey") + .HasColumnType("text"); + + b.Property("PrimaryVersionId") + .HasColumnType("text"); + + b.Property("ProductionLocations") + .HasColumnType("text"); + + b.Property("ProductionYear") + .HasColumnType("integer"); + + b.Property("RunTimeTicks") + .HasColumnType("bigint"); + + b.Property("SeasonId") + .HasColumnType("uuid"); + + b.Property("SeasonName") + .HasColumnType("text"); + + b.Property("SeriesId") + .HasColumnType("uuid"); + + b.Property("SeriesName") + .HasColumnType("text"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("text"); + + b.Property("ShowId") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("SortName") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Studios") + .HasColumnType("text"); + + b.Property("Tagline") + .HasColumnType("text"); + + b.Property("Tags") + .HasColumnType("text"); + + b.Property("TopParentId") + .HasColumnType("uuid"); + + b.Property("TotalBitrate") + .HasColumnType("integer"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnratedType") + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Blurhash") + .HasColumnType("bytea"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("ImageType") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ChapterIndex") + .HasColumnType("integer"); + + b.Property("ImageDateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ImagePath") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("StartPositionTicks") + .HasColumnType("bigint"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChromecastVersion") + .HasColumnType("integer"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("boolean"); + + b.Property("IndexBy") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ScrollDirection") + .HasColumnType("integer"); + + b.Property("ShowBackdrop") + .HasColumnType("boolean"); + + b.Property("ShowSidebar") + .HasColumnType("boolean"); + + b.Property("SkipBackwardLength") + .HasColumnType("integer"); + + b.Property("SkipForwardLength") + .HasColumnType("integer"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DisplayPreferencesId") + .HasColumnType("integer"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IndexBy") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("RememberIndexing") + .HasColumnType("boolean"); + + b.Property("RememberSorting") + .HasColumnType("boolean"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("ViewType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue") + .IsUnique(); + + b.ToTable("ItemValues"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("uuid"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EndTicks") + .HasColumnType("bigint"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartTicks") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("StreamIndex") + .HasColumnType("integer"); + + b.Property("AspectRatio") + .HasColumnType("text"); + + b.Property("AverageFrameRate") + .HasColumnType("real"); + + b.Property("BitDepth") + .HasColumnType("integer"); + + b.Property("BitRate") + .HasColumnType("integer"); + + b.Property("BlPresentFlag") + .HasColumnType("integer"); + + b.Property("ChannelLayout") + .HasColumnType("text"); + + b.Property("Channels") + .HasColumnType("integer"); + + b.Property("Codec") + .HasColumnType("text"); + + b.Property("CodecTag") + .HasColumnType("text"); + + b.Property("CodecTimeBase") + .HasColumnType("text"); + + b.Property("ColorPrimaries") + .HasColumnType("text"); + + b.Property("ColorSpace") + .HasColumnType("text"); + + b.Property("ColorTransfer") + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("integer"); + + b.Property("DvLevel") + .HasColumnType("integer"); + + b.Property("DvProfile") + .HasColumnType("integer"); + + b.Property("DvVersionMajor") + .HasColumnType("integer"); + + b.Property("DvVersionMinor") + .HasColumnType("integer"); + + b.Property("ElPresentFlag") + .HasColumnType("integer"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("IsAnamorphic") + .HasColumnType("boolean"); + + b.Property("IsAvc") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsExternal") + .HasColumnType("boolean"); + + b.Property("IsForced") + .HasColumnType("boolean"); + + b.Property("IsHearingImpaired") + .HasColumnType("boolean"); + + b.Property("IsInterlaced") + .HasColumnType("boolean"); + + b.Property("KeyFrames") + .HasColumnType("text"); + + b.Property("Language") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("real"); + + b.Property("NalLengthSize") + .HasColumnType("text"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("PixelFormat") + .HasColumnType("text"); + + b.Property("Profile") + .HasColumnType("text"); + + b.Property("RealFrameRate") + .HasColumnType("real"); + + b.Property("RefFrames") + .HasColumnType("integer"); + + b.Property("Rotation") + .HasColumnType("integer"); + + b.Property("RpuPresentFlag") + .HasColumnType("integer"); + + b.Property("SampleRate") + .HasColumnType("integer"); + + b.Property("StreamType") + .HasColumnType("integer"); + + b.Property("TimeBase") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PersonType") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("PeopleId") + .HasColumnType("uuid"); + + b.Property("ListOrder") + .HasColumnType("integer"); + + b.Property("Role") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Kind") + .HasColumnType("integer"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Kind") + .HasColumnType("integer"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("character varying(65535)"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastActivity") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastActivity") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CustomName") + .HasColumnType("text"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("Width") + .HasColumnType("integer"); + + b.Property("Bandwidth") + .HasColumnType("integer"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Interval") + .HasColumnType("integer"); + + b.Property("ThumbnailCount") + .HasColumnType("integer"); + + b.Property("TileHeight") + .HasColumnType("integer"); + + b.Property("TileWidth") + .HasColumnType("integer"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DisplayCollectionsView") + .HasColumnType("boolean"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("boolean"); + + b.Property("EnableAutoLogin") + .HasColumnType("boolean"); + + b.Property("EnableLocalPassword") + .HasColumnType("boolean"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("boolean"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("boolean"); + + b.Property("HidePlayedInLatest") + .HasColumnType("boolean"); + + b.Property("InternalId") + .HasColumnType("bigint"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("integer"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("integer"); + + b.Property("MaxActiveSessions") + .HasColumnType("integer"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("integer"); + + b.Property("MustUpdatePassword") + .HasColumnType("boolean"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("character varying(65535)"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("boolean"); + + b.Property("RememberAudioSelections") + .HasColumnType("boolean"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("boolean"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SubtitleMode") + .HasColumnType("integer"); + + b.Property("SyncPlayAccess") + .HasColumnType("integer"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("CustomDataKey") + .HasColumnType("text"); + + b.Property("AudioStreamIndex") + .HasColumnType("integer"); + + b.Property("IsFavorite") + .HasColumnType("boolean"); + + b.Property("LastPlayedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Likes") + .HasColumnType("boolean"); + + b.Property("PlayCount") + .HasColumnType("integer"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("bigint"); + + b.Property("Played") + .HasColumnType("boolean"); + + b.Property("Rating") + .HasColumnType("double precision"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("integer"); + + b.HasKey("ItemId", "UserId", "CustomDataKey"); + + b.HasIndex("UserId"); + + b.HasIndex("ItemId", "UserId", "IsFavorite"); + + b.HasIndex("ItemId", "UserId", "LastPlayedDate"); + + b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("ItemId", "UserId", "Played"); + + b.ToTable("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Children") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem") + .WithMany("ParentAncestors") + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("UserData") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Navigation("Chapters"); + + b.Navigation("Children"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("ParentAncestors"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs new file mode 100644 index 0000000000..f1d0d15647 --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs @@ -0,0 +1,1104 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Jellyfin.Database.Providers.PgSql.Migrations +{ + /// + public partial class InitMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ActivityLogs", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Overview = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + ShortOverview = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + Type = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ItemId = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + DateCreated = table.Column(type: "timestamp with time zone", nullable: false), + LogSeverity = table.Column(type: "integer", nullable: false), + RowVersion = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ActivityLogs", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ApiKeys", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + DateCreated = table.Column(type: "timestamp with time zone", nullable: false), + DateLastActivity = table.Column(type: "timestamp with time zone", nullable: false), + Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + AccessToken = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApiKeys", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "BaseItems", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Type = table.Column(type: "text", nullable: false), + Data = table.Column(type: "text", nullable: true), + Path = table.Column(type: "text", nullable: true), + StartDate = table.Column(type: "timestamp with time zone", nullable: false), + EndDate = table.Column(type: "timestamp with time zone", nullable: false), + ChannelId = table.Column(type: "text", nullable: true), + IsMovie = table.Column(type: "boolean", nullable: false), + CommunityRating = table.Column(type: "real", nullable: true), + CustomRating = table.Column(type: "text", nullable: true), + IndexNumber = table.Column(type: "integer", nullable: true), + IsLocked = table.Column(type: "boolean", nullable: false), + Name = table.Column(type: "text", nullable: true), + OfficialRating = table.Column(type: "text", nullable: true), + MediaType = table.Column(type: "text", nullable: true), + Overview = table.Column(type: "text", nullable: true), + ParentIndexNumber = table.Column(type: "integer", nullable: true), + PremiereDate = table.Column(type: "timestamp with time zone", nullable: true), + ProductionYear = table.Column(type: "integer", nullable: true), + Genres = table.Column(type: "text", nullable: true), + SortName = table.Column(type: "text", nullable: true), + ForcedSortName = table.Column(type: "text", nullable: true), + RunTimeTicks = table.Column(type: "bigint", nullable: true), + DateCreated = table.Column(type: "timestamp with time zone", nullable: true), + DateModified = table.Column(type: "timestamp with time zone", nullable: true), + IsSeries = table.Column(type: "boolean", nullable: false), + EpisodeTitle = table.Column(type: "text", nullable: true), + IsRepeat = table.Column(type: "boolean", nullable: false), + PreferredMetadataLanguage = table.Column(type: "text", nullable: true), + PreferredMetadataCountryCode = table.Column(type: "text", nullable: true), + DateLastRefreshed = table.Column(type: "timestamp with time zone", nullable: true), + DateLastSaved = table.Column(type: "timestamp with time zone", nullable: true), + IsInMixedFolder = table.Column(type: "boolean", nullable: false), + Studios = table.Column(type: "text", nullable: true), + ExternalServiceId = table.Column(type: "text", nullable: true), + Tags = table.Column(type: "text", nullable: true), + IsFolder = table.Column(type: "boolean", nullable: false), + InheritedParentalRatingValue = table.Column(type: "integer", nullable: true), + UnratedType = table.Column(type: "text", nullable: true), + CriticRating = table.Column(type: "real", nullable: true), + CleanName = table.Column(type: "text", nullable: true), + PresentationUniqueKey = table.Column(type: "text", nullable: true), + OriginalTitle = table.Column(type: "text", nullable: true), + PrimaryVersionId = table.Column(type: "text", nullable: true), + DateLastMediaAdded = table.Column(type: "timestamp with time zone", nullable: true), + Album = table.Column(type: "text", nullable: true), + LUFS = table.Column(type: "real", nullable: true), + NormalizationGain = table.Column(type: "real", nullable: true), + IsVirtualItem = table.Column(type: "boolean", nullable: false), + SeriesName = table.Column(type: "text", nullable: true), + SeasonName = table.Column(type: "text", nullable: true), + ExternalSeriesId = table.Column(type: "text", nullable: true), + Tagline = table.Column(type: "text", nullable: true), + ProductionLocations = table.Column(type: "text", nullable: true), + ExtraIds = table.Column(type: "text", nullable: true), + TotalBitrate = table.Column(type: "integer", nullable: true), + ExtraType = table.Column(type: "integer", nullable: true), + Artists = table.Column(type: "text", nullable: true), + AlbumArtists = table.Column(type: "text", nullable: true), + ExternalId = table.Column(type: "text", nullable: true), + SeriesPresentationUniqueKey = table.Column(type: "text", nullable: true), + ShowId = table.Column(type: "text", nullable: true), + OwnerId = table.Column(type: "text", nullable: true), + Width = table.Column(type: "integer", nullable: true), + Height = table.Column(type: "integer", nullable: true), + Size = table.Column(type: "bigint", nullable: true), + Audio = table.Column(type: "integer", nullable: true), + ParentId = table.Column(type: "uuid", nullable: true), + TopParentId = table.Column(type: "uuid", nullable: true), + SeasonId = table.Column(type: "uuid", nullable: true), + SeriesId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BaseItems", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "CustomItemDisplayPreferences", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "uuid", nullable: false), + ItemId = table.Column(type: "uuid", nullable: false), + Client = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Key = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CustomItemDisplayPreferences", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "DeviceOptions", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + DeviceId = table.Column(type: "text", nullable: false), + CustomName = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceOptions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ItemValues", + columns: table => new + { + ItemValueId = table.Column(type: "uuid", nullable: false), + Type = table.Column(type: "integer", nullable: false), + Value = table.Column(type: "text", nullable: false), + CleanValue = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ItemValues", x => x.ItemValueId); + }); + + migrationBuilder.CreateTable( + name: "MediaSegments", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ItemId = table.Column(type: "uuid", nullable: false), + Type = table.Column(type: "integer", nullable: false), + EndTicks = table.Column(type: "bigint", nullable: false), + StartTicks = table.Column(type: "bigint", nullable: false), + SegmentProviderId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MediaSegments", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Peoples", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + PersonType = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Peoples", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "TrickplayInfos", + columns: table => new + { + ItemId = table.Column(type: "uuid", nullable: false), + Width = table.Column(type: "integer", nullable: false), + Height = table.Column(type: "integer", nullable: false), + TileWidth = table.Column(type: "integer", nullable: false), + TileHeight = table.Column(type: "integer", nullable: false), + ThumbnailCount = table.Column(type: "integer", nullable: false), + Interval = table.Column(type: "integer", nullable: false), + Bandwidth = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TrickplayInfos", x => new { x.ItemId, x.Width }); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Username = table.Column(type: "character varying(255)", maxLength: 255, nullable: false, collation: "NOCASE"), + Password = table.Column(type: "character varying(65535)", maxLength: 65535, nullable: true), + MustUpdatePassword = table.Column(type: "boolean", nullable: false), + AudioLanguagePreference = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + AuthenticationProviderId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + PasswordResetProviderId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + InvalidLoginAttemptCount = table.Column(type: "integer", nullable: false), + LastActivityDate = table.Column(type: "timestamp with time zone", nullable: true), + LastLoginDate = table.Column(type: "timestamp with time zone", nullable: true), + LoginAttemptsBeforeLockout = table.Column(type: "integer", nullable: true), + MaxActiveSessions = table.Column(type: "integer", nullable: false), + SubtitleMode = table.Column(type: "integer", nullable: false), + PlayDefaultAudioTrack = table.Column(type: "boolean", nullable: false), + SubtitleLanguagePreference = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + DisplayMissingEpisodes = table.Column(type: "boolean", nullable: false), + DisplayCollectionsView = table.Column(type: "boolean", nullable: false), + EnableLocalPassword = table.Column(type: "boolean", nullable: false), + HidePlayedInLatest = table.Column(type: "boolean", nullable: false), + RememberAudioSelections = table.Column(type: "boolean", nullable: false), + RememberSubtitleSelections = table.Column(type: "boolean", nullable: false), + EnableNextEpisodeAutoPlay = table.Column(type: "boolean", nullable: false), + EnableAutoLogin = table.Column(type: "boolean", nullable: false), + EnableUserPreferenceAccess = table.Column(type: "boolean", nullable: false), + MaxParentalAgeRating = table.Column(type: "integer", nullable: true), + RemoteClientBitrateLimit = table.Column(type: "integer", nullable: true), + InternalId = table.Column(type: "bigint", nullable: false), + SyncPlayAccess = table.Column(type: "integer", nullable: false), + CastReceiverId = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + RowVersion = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AncestorIds", + columns: table => new + { + ParentItemId = table.Column(type: "uuid", nullable: false), + ItemId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AncestorIds", x => new { x.ItemId, x.ParentItemId }); + table.ForeignKey( + name: "FK_AncestorIds_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AncestorIds_BaseItems_ParentItemId", + column: x => x.ParentItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AttachmentStreamInfos", + columns: table => new + { + ItemId = table.Column(type: "uuid", nullable: false), + Index = table.Column(type: "integer", nullable: false), + Codec = table.Column(type: "text", nullable: false), + CodecTag = table.Column(type: "text", nullable: true), + Comment = table.Column(type: "text", nullable: true), + Filename = table.Column(type: "text", nullable: true), + MimeType = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AttachmentStreamInfos", x => new { x.ItemId, x.Index }); + table.ForeignKey( + name: "FK_AttachmentStreamInfos_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "BaseItemImageInfos", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Path = table.Column(type: "text", nullable: false), + DateModified = table.Column(type: "timestamp with time zone", nullable: false), + ImageType = table.Column(type: "integer", nullable: false), + Width = table.Column(type: "integer", nullable: false), + Height = table.Column(type: "integer", nullable: false), + Blurhash = table.Column(type: "bytea", nullable: true), + ItemId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BaseItemImageInfos", x => x.Id); + table.ForeignKey( + name: "FK_BaseItemImageInfos_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "BaseItemMetadataFields", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false), + ItemId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BaseItemMetadataFields", x => new { x.Id, x.ItemId }); + table.ForeignKey( + name: "FK_BaseItemMetadataFields_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "BaseItemProviders", + columns: table => new + { + ItemId = table.Column(type: "uuid", nullable: false), + ProviderId = table.Column(type: "text", nullable: false), + ProviderValue = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BaseItemProviders", x => new { x.ItemId, x.ProviderId }); + table.ForeignKey( + name: "FK_BaseItemProviders_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "BaseItemTrailerTypes", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false), + ItemId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BaseItemTrailerTypes", x => new { x.Id, x.ItemId }); + table.ForeignKey( + name: "FK_BaseItemTrailerTypes_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Chapters", + columns: table => new + { + ItemId = table.Column(type: "uuid", nullable: false), + ChapterIndex = table.Column(type: "integer", nullable: false), + StartPositionTicks = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "text", nullable: true), + ImagePath = table.Column(type: "text", nullable: true), + ImageDateModified = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Chapters", x => new { x.ItemId, x.ChapterIndex }); + table.ForeignKey( + name: "FK_Chapters_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MediaStreamInfos", + columns: table => new + { + ItemId = table.Column(type: "uuid", nullable: false), + StreamIndex = table.Column(type: "integer", nullable: false), + StreamType = table.Column(type: "integer", nullable: false), + Codec = table.Column(type: "text", nullable: true), + Language = table.Column(type: "text", nullable: true), + ChannelLayout = table.Column(type: "text", nullable: true), + Profile = table.Column(type: "text", nullable: true), + AspectRatio = table.Column(type: "text", nullable: true), + Path = table.Column(type: "text", nullable: true), + IsInterlaced = table.Column(type: "boolean", nullable: true), + BitRate = table.Column(type: "integer", nullable: true), + Channels = table.Column(type: "integer", nullable: true), + SampleRate = table.Column(type: "integer", nullable: true), + IsDefault = table.Column(type: "boolean", nullable: false), + IsForced = table.Column(type: "boolean", nullable: false), + IsExternal = table.Column(type: "boolean", nullable: false), + Height = table.Column(type: "integer", nullable: true), + Width = table.Column(type: "integer", nullable: true), + AverageFrameRate = table.Column(type: "real", nullable: true), + RealFrameRate = table.Column(type: "real", nullable: true), + Level = table.Column(type: "real", nullable: true), + PixelFormat = table.Column(type: "text", nullable: true), + BitDepth = table.Column(type: "integer", nullable: true), + IsAnamorphic = table.Column(type: "boolean", nullable: true), + RefFrames = table.Column(type: "integer", nullable: true), + CodecTag = table.Column(type: "text", nullable: true), + Comment = table.Column(type: "text", nullable: true), + NalLengthSize = table.Column(type: "text", nullable: true), + IsAvc = table.Column(type: "boolean", nullable: true), + Title = table.Column(type: "text", nullable: true), + TimeBase = table.Column(type: "text", nullable: true), + CodecTimeBase = table.Column(type: "text", nullable: true), + ColorPrimaries = table.Column(type: "text", nullable: true), + ColorSpace = table.Column(type: "text", nullable: true), + ColorTransfer = table.Column(type: "text", nullable: true), + DvVersionMajor = table.Column(type: "integer", nullable: true), + DvVersionMinor = table.Column(type: "integer", nullable: true), + DvProfile = table.Column(type: "integer", nullable: true), + DvLevel = table.Column(type: "integer", nullable: true), + RpuPresentFlag = table.Column(type: "integer", nullable: true), + ElPresentFlag = table.Column(type: "integer", nullable: true), + BlPresentFlag = table.Column(type: "integer", nullable: true), + DvBlSignalCompatibilityId = table.Column(type: "integer", nullable: true), + IsHearingImpaired = table.Column(type: "boolean", nullable: true), + Rotation = table.Column(type: "integer", nullable: true), + KeyFrames = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MediaStreamInfos", x => new { x.ItemId, x.StreamIndex }); + table.ForeignKey( + name: "FK_MediaStreamInfos_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ItemValuesMap", + columns: table => new + { + ItemId = table.Column(type: "uuid", nullable: false), + ItemValueId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ItemValuesMap", x => new { x.ItemValueId, x.ItemId }); + table.ForeignKey( + name: "FK_ItemValuesMap_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ItemValuesMap_ItemValues_ItemValueId", + column: x => x.ItemValueId, + principalTable: "ItemValues", + principalColumn: "ItemValueId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PeopleBaseItemMap", + columns: table => new + { + ItemId = table.Column(type: "uuid", nullable: false), + PeopleId = table.Column(type: "uuid", nullable: false), + SortOrder = table.Column(type: "integer", nullable: true), + ListOrder = table.Column(type: "integer", nullable: true), + Role = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PeopleBaseItemMap", x => new { x.ItemId, x.PeopleId }); + table.ForeignKey( + name: "FK_PeopleBaseItemMap_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PeopleBaseItemMap_Peoples_PeopleId", + column: x => x.PeopleId, + principalTable: "Peoples", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AccessSchedules", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "uuid", nullable: false), + DayOfWeek = table.Column(type: "integer", nullable: false), + StartHour = table.Column(type: "double precision", nullable: false), + EndHour = table.Column(type: "double precision", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AccessSchedules", x => x.Id); + table.ForeignKey( + name: "FK_AccessSchedules_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Devices", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "uuid", nullable: false), + AccessToken = table.Column(type: "text", nullable: false), + AppName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + AppVersion = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + DeviceName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + DeviceId = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + DateCreated = table.Column(type: "timestamp with time zone", nullable: false), + DateModified = table.Column(type: "timestamp with time zone", nullable: false), + DateLastActivity = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Devices", x => x.Id); + table.ForeignKey( + name: "FK_Devices_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "DisplayPreferences", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "uuid", nullable: false), + ItemId = table.Column(type: "uuid", nullable: false), + Client = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + ShowSidebar = table.Column(type: "boolean", nullable: false), + ShowBackdrop = table.Column(type: "boolean", nullable: false), + ScrollDirection = table.Column(type: "integer", nullable: false), + IndexBy = table.Column(type: "integer", nullable: true), + SkipForwardLength = table.Column(type: "integer", nullable: false), + SkipBackwardLength = table.Column(type: "integer", nullable: false), + ChromecastVersion = table.Column(type: "integer", nullable: false), + EnableNextVideoInfoOverlay = table.Column(type: "boolean", nullable: false), + DashboardTheme = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), + TvHome = table.Column(type: "character varying(32)", maxLength: 32, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DisplayPreferences", x => x.Id); + table.ForeignKey( + name: "FK_DisplayPreferences_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ImageInfos", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "uuid", nullable: true), + Path = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + LastModified = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ImageInfos", x => x.Id); + table.ForeignKey( + name: "FK_ImageInfos_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ItemDisplayPreferences", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "uuid", nullable: false), + ItemId = table.Column(type: "uuid", nullable: false), + Client = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + ViewType = table.Column(type: "integer", nullable: false), + RememberIndexing = table.Column(type: "boolean", nullable: false), + IndexBy = table.Column(type: "integer", nullable: true), + RememberSorting = table.Column(type: "boolean", nullable: false), + SortBy = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + SortOrder = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ItemDisplayPreferences", x => x.Id); + table.ForeignKey( + name: "FK_ItemDisplayPreferences_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Permissions", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "uuid", nullable: true), + Kind = table.Column(type: "integer", nullable: false), + Value = table.Column(type: "boolean", nullable: false), + RowVersion = table.Column(type: "bigint", nullable: false), + Permission_Permissions_Guid = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Permissions", x => x.Id); + table.ForeignKey( + name: "FK_Permissions_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Preferences", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "uuid", nullable: true), + Kind = table.Column(type: "integer", nullable: false), + Value = table.Column(type: "character varying(65535)", maxLength: 65535, nullable: false), + RowVersion = table.Column(type: "bigint", nullable: false), + Preference_Preferences_Guid = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Preferences", x => x.Id); + table.ForeignKey( + name: "FK_Preferences_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserData", + columns: table => new + { + CustomDataKey = table.Column(type: "text", nullable: false), + ItemId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Rating = table.Column(type: "double precision", nullable: true), + PlaybackPositionTicks = table.Column(type: "bigint", nullable: false), + PlayCount = table.Column(type: "integer", nullable: false), + IsFavorite = table.Column(type: "boolean", nullable: false), + LastPlayedDate = table.Column(type: "timestamp with time zone", nullable: true), + Played = table.Column(type: "boolean", nullable: false), + AudioStreamIndex = table.Column(type: "integer", nullable: true), + SubtitleStreamIndex = table.Column(type: "integer", nullable: true), + Likes = table.Column(type: "boolean", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserData", x => new { x.ItemId, x.UserId, x.CustomDataKey }); + table.ForeignKey( + name: "FK_UserData_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserData_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "HomeSection", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + DisplayPreferencesId = table.Column(type: "integer", nullable: false), + Order = table.Column(type: "integer", nullable: false), + Type = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_HomeSection", x => x.Id); + table.ForeignKey( + name: "FK_HomeSection_DisplayPreferences_DisplayPreferencesId", + column: x => x.DisplayPreferencesId, + principalTable: "DisplayPreferences", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AccessSchedules_UserId", + table: "AccessSchedules", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_ActivityLogs_DateCreated", + table: "ActivityLogs", + column: "DateCreated"); + + migrationBuilder.CreateIndex( + name: "IX_AncestorIds_ParentItemId", + table: "AncestorIds", + column: "ParentItemId"); + + migrationBuilder.CreateIndex( + name: "IX_ApiKeys_AccessToken", + table: "ApiKeys", + column: "AccessToken", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_BaseItemImageInfos_ItemId", + table: "BaseItemImageInfos", + column: "ItemId"); + + migrationBuilder.CreateIndex( + name: "IX_BaseItemMetadataFields_ItemId", + table: "BaseItemMetadataFields", + column: "ItemId"); + + migrationBuilder.CreateIndex( + name: "IX_BaseItemProviders_ProviderId_ProviderValue_ItemId", + table: "BaseItemProviders", + columns: new[] { "ProviderId", "ProviderValue", "ItemId" }); + + migrationBuilder.CreateIndex( + name: "IX_BaseItems_Id_Type_IsFolder_IsVirtualItem", + table: "BaseItems", + columns: new[] { "Id", "Type", "IsFolder", "IsVirtualItem" }); + + migrationBuilder.CreateIndex( + name: "IX_BaseItems_IsFolder_TopParentId_IsVirtualItem_PresentationUn~", + table: "BaseItems", + columns: new[] { "IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated" }); + + migrationBuilder.CreateIndex( + name: "IX_BaseItems_MediaType_TopParentId_IsVirtualItem_PresentationU~", + table: "BaseItems", + columns: new[] { "MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey" }); + + migrationBuilder.CreateIndex( + name: "IX_BaseItems_ParentId", + table: "BaseItems", + column: "ParentId"); + + migrationBuilder.CreateIndex( + name: "IX_BaseItems_Path", + table: "BaseItems", + column: "Path"); + + migrationBuilder.CreateIndex( + name: "IX_BaseItems_PresentationUniqueKey", + table: "BaseItems", + column: "PresentationUniqueKey"); + + migrationBuilder.CreateIndex( + name: "IX_BaseItems_TopParentId_Id", + table: "BaseItems", + columns: new[] { "TopParentId", "Id" }); + + migrationBuilder.CreateIndex( + name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_IsFolder_IsVirtu~", + table: "BaseItems", + columns: new[] { "Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem" }); + + migrationBuilder.CreateIndex( + name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_PresentationUniq~", + table: "BaseItems", + columns: new[] { "Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName" }); + + migrationBuilder.CreateIndex( + name: "IX_BaseItems_Type_TopParentId_Id", + table: "BaseItems", + columns: new[] { "Type", "TopParentId", "Id" }); + + migrationBuilder.CreateIndex( + name: "IX_BaseItems_Type_TopParentId_IsVirtualItem_PresentationUnique~", + table: "BaseItems", + columns: new[] { "Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated" }); + + migrationBuilder.CreateIndex( + name: "IX_BaseItems_Type_TopParentId_PresentationUniqueKey", + table: "BaseItems", + columns: new[] { "Type", "TopParentId", "PresentationUniqueKey" }); + + migrationBuilder.CreateIndex( + name: "IX_BaseItems_Type_TopParentId_StartDate", + table: "BaseItems", + columns: new[] { "Type", "TopParentId", "StartDate" }); + + migrationBuilder.CreateIndex( + name: "IX_BaseItemTrailerTypes_ItemId", + table: "BaseItemTrailerTypes", + column: "ItemId"); + + migrationBuilder.CreateIndex( + name: "IX_CustomItemDisplayPreferences_UserId_ItemId_Client_Key", + table: "CustomItemDisplayPreferences", + columns: new[] { "UserId", "ItemId", "Client", "Key" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DeviceOptions_DeviceId", + table: "DeviceOptions", + column: "DeviceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Devices_AccessToken_DateLastActivity", + table: "Devices", + columns: new[] { "AccessToken", "DateLastActivity" }); + + migrationBuilder.CreateIndex( + name: "IX_Devices_DeviceId", + table: "Devices", + column: "DeviceId"); + + migrationBuilder.CreateIndex( + name: "IX_Devices_DeviceId_DateLastActivity", + table: "Devices", + columns: new[] { "DeviceId", "DateLastActivity" }); + + migrationBuilder.CreateIndex( + name: "IX_Devices_UserId_DeviceId", + table: "Devices", + columns: new[] { "UserId", "DeviceId" }); + + migrationBuilder.CreateIndex( + name: "IX_DisplayPreferences_UserId_ItemId_Client", + table: "DisplayPreferences", + columns: new[] { "UserId", "ItemId", "Client" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_HomeSection_DisplayPreferencesId", + table: "HomeSection", + column: "DisplayPreferencesId"); + + migrationBuilder.CreateIndex( + name: "IX_ImageInfos_UserId", + table: "ImageInfos", + column: "UserId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ItemDisplayPreferences_UserId", + table: "ItemDisplayPreferences", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_ItemValues_Type_CleanValue", + table: "ItemValues", + columns: new[] { "Type", "CleanValue" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ItemValuesMap_ItemId", + table: "ItemValuesMap", + column: "ItemId"); + + migrationBuilder.CreateIndex( + name: "IX_MediaStreamInfos_StreamIndex", + table: "MediaStreamInfos", + column: "StreamIndex"); + + migrationBuilder.CreateIndex( + name: "IX_MediaStreamInfos_StreamIndex_StreamType", + table: "MediaStreamInfos", + columns: new[] { "StreamIndex", "StreamType" }); + + migrationBuilder.CreateIndex( + name: "IX_MediaStreamInfos_StreamIndex_StreamType_Language", + table: "MediaStreamInfos", + columns: new[] { "StreamIndex", "StreamType", "Language" }); + + migrationBuilder.CreateIndex( + name: "IX_MediaStreamInfos_StreamType", + table: "MediaStreamInfos", + column: "StreamType"); + + migrationBuilder.CreateIndex( + name: "IX_PeopleBaseItemMap_ItemId_ListOrder", + table: "PeopleBaseItemMap", + columns: new[] { "ItemId", "ListOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_PeopleBaseItemMap_ItemId_SortOrder", + table: "PeopleBaseItemMap", + columns: new[] { "ItemId", "SortOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_PeopleBaseItemMap_PeopleId", + table: "PeopleBaseItemMap", + column: "PeopleId"); + + migrationBuilder.CreateIndex( + name: "IX_Peoples_Name", + table: "Peoples", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_Permissions_UserId_Kind", + table: "Permissions", + columns: new[] { "UserId", "Kind" }, + unique: true, + filter: "[UserId] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_Preferences_UserId_Kind", + table: "Preferences", + columns: new[] { "UserId", "Kind" }, + unique: true, + filter: "[UserId] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_UserData_ItemId_UserId_IsFavorite", + table: "UserData", + columns: new[] { "ItemId", "UserId", "IsFavorite" }); + + migrationBuilder.CreateIndex( + name: "IX_UserData_ItemId_UserId_LastPlayedDate", + table: "UserData", + columns: new[] { "ItemId", "UserId", "LastPlayedDate" }); + + migrationBuilder.CreateIndex( + name: "IX_UserData_ItemId_UserId_PlaybackPositionTicks", + table: "UserData", + columns: new[] { "ItemId", "UserId", "PlaybackPositionTicks" }); + + migrationBuilder.CreateIndex( + name: "IX_UserData_ItemId_UserId_Played", + table: "UserData", + columns: new[] { "ItemId", "UserId", "Played" }); + + migrationBuilder.CreateIndex( + name: "IX_UserData_UserId", + table: "UserData", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + table: "Users", + column: "Username", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AccessSchedules"); + + migrationBuilder.DropTable( + name: "ActivityLogs"); + + migrationBuilder.DropTable( + name: "AncestorIds"); + + migrationBuilder.DropTable( + name: "ApiKeys"); + + migrationBuilder.DropTable( + name: "AttachmentStreamInfos"); + + migrationBuilder.DropTable( + name: "BaseItemImageInfos"); + + migrationBuilder.DropTable( + name: "BaseItemMetadataFields"); + + migrationBuilder.DropTable( + name: "BaseItemProviders"); + + migrationBuilder.DropTable( + name: "BaseItemTrailerTypes"); + + migrationBuilder.DropTable( + name: "Chapters"); + + migrationBuilder.DropTable( + name: "CustomItemDisplayPreferences"); + + migrationBuilder.DropTable( + name: "DeviceOptions"); + + migrationBuilder.DropTable( + name: "Devices"); + + migrationBuilder.DropTable( + name: "HomeSection"); + + migrationBuilder.DropTable( + name: "ImageInfos"); + + migrationBuilder.DropTable( + name: "ItemDisplayPreferences"); + + migrationBuilder.DropTable( + name: "ItemValuesMap"); + + migrationBuilder.DropTable( + name: "MediaSegments"); + + migrationBuilder.DropTable( + name: "MediaStreamInfos"); + + migrationBuilder.DropTable( + name: "PeopleBaseItemMap"); + + migrationBuilder.DropTable( + name: "Permissions"); + + migrationBuilder.DropTable( + name: "Preferences"); + + migrationBuilder.DropTable( + name: "TrickplayInfos"); + + migrationBuilder.DropTable( + name: "UserData"); + + migrationBuilder.DropTable( + name: "DisplayPreferences"); + + migrationBuilder.DropTable( + name: "ItemValues"); + + migrationBuilder.DropTable( + name: "Peoples"); + + migrationBuilder.DropTable( + name: "BaseItems"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs new file mode 100644 index 0000000000..cdaf257d4d --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs @@ -0,0 +1,1621 @@ +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Jellyfin.Database.Providers.PgSql.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + partial class JellyfinDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("EndHour") + .HasColumnType("double precision"); + + b.Property("StartHour") + .HasColumnType("double precision"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LogSeverity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ParentItemId") + .HasColumnType("uuid"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("text"); + + b.Property("CodecTag") + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("Filename") + .HasColumnType("text"); + + b.Property("MimeType") + .HasColumnType("text"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Album") + .HasColumnType("text"); + + b.Property("AlbumArtists") + .HasColumnType("text"); + + b.Property("Artists") + .HasColumnType("text"); + + b.Property("Audio") + .HasColumnType("integer"); + + b.Property("ChannelId") + .HasColumnType("text"); + + b.Property("CleanName") + .HasColumnType("text"); + + b.Property("CommunityRating") + .HasColumnType("real"); + + b.Property("CriticRating") + .HasColumnType("real"); + + b.Property("CustomRating") + .HasColumnType("text"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastMediaAdded") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastRefreshed") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastSaved") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EpisodeTitle") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasColumnType("text"); + + b.Property("ExternalSeriesId") + .HasColumnType("text"); + + b.Property("ExternalServiceId") + .HasColumnType("text"); + + b.Property("ExtraIds") + .HasColumnType("text"); + + b.Property("ExtraType") + .HasColumnType("integer"); + + b.Property("ForcedSortName") + .HasColumnType("text"); + + b.Property("Genres") + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("IndexNumber") + .HasColumnType("integer"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("integer"); + + b.Property("IsFolder") + .HasColumnType("boolean"); + + b.Property("IsInMixedFolder") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsMovie") + .HasColumnType("boolean"); + + b.Property("IsRepeat") + .HasColumnType("boolean"); + + b.Property("IsSeries") + .HasColumnType("boolean"); + + b.Property("IsVirtualItem") + .HasColumnType("boolean"); + + b.Property("LUFS") + .HasColumnType("real"); + + b.Property("MediaType") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NormalizationGain") + .HasColumnType("real"); + + b.Property("OfficialRating") + .HasColumnType("text"); + + b.Property("OriginalTitle") + .HasColumnType("text"); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("ParentIndexNumber") + .HasColumnType("integer"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("text"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("text"); + + b.Property("PremiereDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PresentationUniqueKey") + .HasColumnType("text"); + + b.Property("PrimaryVersionId") + .HasColumnType("text"); + + b.Property("ProductionLocations") + .HasColumnType("text"); + + b.Property("ProductionYear") + .HasColumnType("integer"); + + b.Property("RunTimeTicks") + .HasColumnType("bigint"); + + b.Property("SeasonId") + .HasColumnType("uuid"); + + b.Property("SeasonName") + .HasColumnType("text"); + + b.Property("SeriesId") + .HasColumnType("uuid"); + + b.Property("SeriesName") + .HasColumnType("text"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("text"); + + b.Property("ShowId") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("SortName") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Studios") + .HasColumnType("text"); + + b.Property("Tagline") + .HasColumnType("text"); + + b.Property("Tags") + .HasColumnType("text"); + + b.Property("TopParentId") + .HasColumnType("uuid"); + + b.Property("TotalBitrate") + .HasColumnType("integer"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnratedType") + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Blurhash") + .HasColumnType("bytea"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("ImageType") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ChapterIndex") + .HasColumnType("integer"); + + b.Property("ImageDateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ImagePath") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("StartPositionTicks") + .HasColumnType("bigint"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChromecastVersion") + .HasColumnType("integer"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("boolean"); + + b.Property("IndexBy") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ScrollDirection") + .HasColumnType("integer"); + + b.Property("ShowBackdrop") + .HasColumnType("boolean"); + + b.Property("ShowSidebar") + .HasColumnType("boolean"); + + b.Property("SkipBackwardLength") + .HasColumnType("integer"); + + b.Property("SkipForwardLength") + .HasColumnType("integer"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DisplayPreferencesId") + .HasColumnType("integer"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IndexBy") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("RememberIndexing") + .HasColumnType("boolean"); + + b.Property("RememberSorting") + .HasColumnType("boolean"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("ViewType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue") + .IsUnique(); + + b.ToTable("ItemValues"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("uuid"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EndTicks") + .HasColumnType("bigint"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartTicks") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("StreamIndex") + .HasColumnType("integer"); + + b.Property("AspectRatio") + .HasColumnType("text"); + + b.Property("AverageFrameRate") + .HasColumnType("real"); + + b.Property("BitDepth") + .HasColumnType("integer"); + + b.Property("BitRate") + .HasColumnType("integer"); + + b.Property("BlPresentFlag") + .HasColumnType("integer"); + + b.Property("ChannelLayout") + .HasColumnType("text"); + + b.Property("Channels") + .HasColumnType("integer"); + + b.Property("Codec") + .HasColumnType("text"); + + b.Property("CodecTag") + .HasColumnType("text"); + + b.Property("CodecTimeBase") + .HasColumnType("text"); + + b.Property("ColorPrimaries") + .HasColumnType("text"); + + b.Property("ColorSpace") + .HasColumnType("text"); + + b.Property("ColorTransfer") + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("integer"); + + b.Property("DvLevel") + .HasColumnType("integer"); + + b.Property("DvProfile") + .HasColumnType("integer"); + + b.Property("DvVersionMajor") + .HasColumnType("integer"); + + b.Property("DvVersionMinor") + .HasColumnType("integer"); + + b.Property("ElPresentFlag") + .HasColumnType("integer"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("IsAnamorphic") + .HasColumnType("boolean"); + + b.Property("IsAvc") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsExternal") + .HasColumnType("boolean"); + + b.Property("IsForced") + .HasColumnType("boolean"); + + b.Property("IsHearingImpaired") + .HasColumnType("boolean"); + + b.Property("IsInterlaced") + .HasColumnType("boolean"); + + b.Property("KeyFrames") + .HasColumnType("text"); + + b.Property("Language") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("real"); + + b.Property("NalLengthSize") + .HasColumnType("text"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("PixelFormat") + .HasColumnType("text"); + + b.Property("Profile") + .HasColumnType("text"); + + b.Property("RealFrameRate") + .HasColumnType("real"); + + b.Property("RefFrames") + .HasColumnType("integer"); + + b.Property("Rotation") + .HasColumnType("integer"); + + b.Property("RpuPresentFlag") + .HasColumnType("integer"); + + b.Property("SampleRate") + .HasColumnType("integer"); + + b.Property("StreamType") + .HasColumnType("integer"); + + b.Property("TimeBase") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PersonType") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("PeopleId") + .HasColumnType("uuid"); + + b.Property("ListOrder") + .HasColumnType("integer"); + + b.Property("Role") + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Kind") + .HasColumnType("integer"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Value") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Kind") + .HasColumnType("integer"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("character varying(65535)"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastActivity") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastActivity") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CustomName") + .HasColumnType("text"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("Width") + .HasColumnType("integer"); + + b.Property("Bandwidth") + .HasColumnType("integer"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Interval") + .HasColumnType("integer"); + + b.Property("ThumbnailCount") + .HasColumnType("integer"); + + b.Property("TileHeight") + .HasColumnType("integer"); + + b.Property("TileWidth") + .HasColumnType("integer"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DisplayCollectionsView") + .HasColumnType("boolean"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("boolean"); + + b.Property("EnableAutoLogin") + .HasColumnType("boolean"); + + b.Property("EnableLocalPassword") + .HasColumnType("boolean"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("boolean"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("boolean"); + + b.Property("HidePlayedInLatest") + .HasColumnType("boolean"); + + b.Property("InternalId") + .HasColumnType("bigint"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("integer"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("integer"); + + b.Property("MaxActiveSessions") + .HasColumnType("integer"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("integer"); + + b.Property("MustUpdatePassword") + .HasColumnType("boolean"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("character varying(65535)"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("boolean"); + + b.Property("RememberAudioSelections") + .HasColumnType("boolean"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("boolean"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SubtitleMode") + .HasColumnType("integer"); + + b.Property("SyncPlayAccess") + .HasColumnType("integer"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("CustomDataKey") + .HasColumnType("text"); + + b.Property("AudioStreamIndex") + .HasColumnType("integer"); + + b.Property("IsFavorite") + .HasColumnType("boolean"); + + b.Property("LastPlayedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Likes") + .HasColumnType("boolean"); + + b.Property("PlayCount") + .HasColumnType("integer"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("bigint"); + + b.Property("Played") + .HasColumnType("boolean"); + + b.Property("Rating") + .HasColumnType("double precision"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("integer"); + + b.HasKey("ItemId", "UserId", "CustomDataKey"); + + b.HasIndex("UserId"); + + b.HasIndex("ItemId", "UserId", "IsFavorite"); + + b.HasIndex("ItemId", "UserId", "LastPlayedDate"); + + b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("ItemId", "UserId", "Played"); + + b.ToTable("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Children") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem") + .WithMany("ParentAncestors") + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("UserData") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Navigation("Chapters"); + + b.Navigation("Children"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("ParentAncestors"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/PgSqlDesignTimeJellyfinDbFactory.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/PgSqlDesignTimeJellyfinDbFactory.cs new file mode 100644 index 0000000000..8f5e2e82b2 --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/PgSqlDesignTimeJellyfinDbFactory.cs @@ -0,0 +1,26 @@ +using Jellyfin.Database.Providers.SqLite; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Jellyfin.Database.Providers.PgSql +{ + /// + /// The design time factory for . + /// This is only used for the creation of migrations and not during runtime. + /// + internal sealed class PgSqlDesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory + { + public JellyfinDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql(f => f.MigrationsAssembly(GetType().Assembly)); + + return new JellyfinDbContext( + optionsBuilder.Options, + NullLogger.Instance, + new SqliteDatabaseProvider(null!, NullLogger.Instance)); + } + } +} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs new file mode 100644 index 0000000000..1dae3401bc --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs @@ -0,0 +1,75 @@ +using System; +using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.DatabaseConfiguration; +using MediaBrowser.Common.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Npgsql; + +namespace Jellyfin.Database.Providers.PgSql; + +/// +/// Configures jellyfin to use an SqLite database. +/// +[JellyfinDatabaseProviderKey("Jellyfin-PgSql")] +public sealed class PgSqlDatabaseProvider : IJellyfinDatabaseProvider +{ + private readonly IConfigurationManager _configurationManager; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Configuration manager to get PgSQL connection data. + /// A logger. + public PgSqlDatabaseProvider(IConfigurationManager configurationManager, ILogger logger) + { + _configurationManager = configurationManager; + _logger = logger; + } + + /// + public IDbContextFactory? DbContextFactory { get; set; } + + /// + public void Initialise(DbContextOptionsBuilder options) + { + var dbSettings = _configurationManager.GetConfiguration("database"); + + if (dbSettings.PostgreSql is null) + { + throw new InvalidOperationException("Selected PgSQL as database provider but did not provide required configuration. Please see docs."); + } + + var connectionBuilder = new NpgsqlConnectionStringBuilder(); + connectionBuilder.ApplicationName = "jellyfin"; + connectionBuilder.CommandTimeout = dbSettings.PostgreSql.Timeout; + connectionBuilder.Database = dbSettings.PostgreSql.DatabaseName; + connectionBuilder.Username = dbSettings.PostgreSql.Username; + connectionBuilder.Password = dbSettings.PostgreSql.Password; + connectionBuilder.Host = dbSettings.PostgreSql.ServerName; + connectionBuilder.Port = dbSettings.PostgreSql.Port; + + var connectionString = connectionBuilder.ToString(); + + options + .UseNpgsql(connectionString, pgSqlOptions => pgSqlOptions.MigrationsAssembly(GetType().Assembly)); + } + + /// + public Task RunScheduledOptimisation(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + /// + public void OnModelCreating(ModelBuilder modelBuilder) + { + } + + /// + public ValueTask DisposeAsync() + { + return ValueTask.CompletedTask; + } +} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs similarity index 89% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs rename to Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs index fdd9a51361..11eeb8e02d 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/DesignTimeJellyfinDbFactory.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Server.Implementations.Migrations /// The design time factory for . /// This is only used for the creation of migrations and not during runtime. /// - internal sealed class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory + internal sealed class SqliteDesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory { public JellyfinDbContext CreateDbContext(string[] args) { diff --git a/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs b/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs index af2ede7010..d49d8536a3 100644 --- a/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs +++ b/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs @@ -11,4 +11,9 @@ public class DatabaseConfigurationOptions /// Gets or Sets the type of database jellyfin should use. /// public required string DatabaseType { get; set; } + + /// + /// Gets or Sets the settings to run jellyfin with Postgres. + /// + public PostgreSqlOptions? PostgreSql { get; set; } } diff --git a/Jellyfin.Server.Implementations/DbConfiguration/PostgreSqlOptions.cs b/Jellyfin.Server.Implementations/DbConfiguration/PostgreSqlOptions.cs new file mode 100644 index 0000000000..1f7c30b098 --- /dev/null +++ b/Jellyfin.Server.Implementations/DbConfiguration/PostgreSqlOptions.cs @@ -0,0 +1,39 @@ +using System; + +namespace Jellyfin.Server.Implementations.DatabaseConfiguration; + +/// +/// Options specific to run jellyfin on a postgreSql database. +/// +public class PostgreSqlOptions +{ + /// + /// Gets or Sets the Port. Defaults to 5432. + /// + public required int Port { get; set; } = 5432; + + /// + /// Gets or Sets the Server name. + /// + public required string ServerName { get; set; } + + /// + /// Gets or Sets the username. + /// + public required string Username { get; set; } + + /// + /// Gets or Sets the password. + /// + public required string Password { get; set; } + + /// + /// Gets or Sets the database name. Defaults to "Jellyfin". + /// + public string DatabaseName { get; set; } = "Jellyfin"; + + /// + /// Gets or Sets the timeout in secounds before a running command is terminated. Defaults to 30. + /// + public int Timeout { get; set; } = 30; +} From ce00bc076e9a97197e7e7e83276013518ce84ec5 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 27 Jan 2025 18:21:47 +0000 Subject: [PATCH 016/508] Fixed postgres sql provider --- .devcontainer/devcontainer.json | 20 ++++--------------- .../DatabaseConfigurationOptions.cs | 0 .../DbConfiguration/PostgreSqlOptions.cs | 0 .../Jellyfin.Database.Providers.PgSql.csproj | 1 - .../PgSqlDesignTimeJellyfinDbFactory.cs | 3 +-- .../Extensions/ServiceCollectionExtensions.cs | 3 +++ .../Jellyfin.Server.Implementations.csproj | 1 + 7 files changed, 9 insertions(+), 19 deletions(-) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/DbConfiguration/DatabaseConfigurationOptions.cs (100%) rename {Jellyfin.Server.Implementations => Jellyfin.Database/Jellyfin.Database.Implementations}/DbConfiguration/PostgreSqlOptions.cs (100%) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bcf484463b..84c1dda971 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Development Jellyfin Server", - "image":"mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm", + "image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm", // restores nuget packages, installs the dotnet workloads and installs the dev https certificate "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"", // reads the extensions list and installs them @@ -13,7 +13,9 @@ }, "ghcr.io/devcontainers-contrib/features/apt-packages:1": { "preserve_apt_list": false, - "packages": ["libfontconfig1"] + "packages": [ + "libfontconfig1" + ] }, "ghcr.io/devcontainers/features/docker-in-docker:2": { "dockerDashComposeVersion": "v2" @@ -24,19 +26,5 @@ "hostRequirements": { "memory": "8gb", "cpus": 4 - }, "remoteEnv": { - "JELLYFIN_DATA_DIR": "/config" - }, - "mounts": [ - "source=/opt/docker/data/jellyfin/testConfig/,target=/config,type=bind,consistency=cached", - "source=/opt/docker/data/jellyfin/config10.9.11/metadata,target=/config/metadata,type=bind,consistency=cached", - "source=/mnt/video,target=/media,type=bind,consistency=cached" - ], - "customizations": { - "vscode": { - "extensions": [ - "alexcvzz.vscode-sqlite" - ] - } } } diff --git a/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs similarity index 100% rename from Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs diff --git a/Jellyfin.Server.Implementations/DbConfiguration/PostgreSqlOptions.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/PostgreSqlOptions.cs similarity index 100% rename from Jellyfin.Server.Implementations/DbConfiguration/PostgreSqlOptions.cs rename to Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/PostgreSqlOptions.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Jellyfin.Database.Providers.PgSql.csproj b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Jellyfin.Database.Providers.PgSql.csproj index ae1497403b..785a3c63ab 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Jellyfin.Database.Providers.PgSql.csproj +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Jellyfin.Database.Providers.PgSql.csproj @@ -45,7 +45,6 @@ - diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/PgSqlDesignTimeJellyfinDbFactory.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/PgSqlDesignTimeJellyfinDbFactory.cs index 8f5e2e82b2..bf949d570b 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/PgSqlDesignTimeJellyfinDbFactory.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/PgSqlDesignTimeJellyfinDbFactory.cs @@ -1,4 +1,3 @@ -using Jellyfin.Database.Providers.SqLite; using Jellyfin.Server.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; @@ -20,7 +19,7 @@ namespace Jellyfin.Database.Providers.PgSql return new JellyfinDbContext( optionsBuilder.Options, NullLogger.Instance, - new SqliteDatabaseProvider(null!, NullLogger.Instance)); + new PgSqlDatabaseProvider(null!, NullLogger.Instance)); } } } diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index 091ecee987..7936c6fd98 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using Jellyfin.Database.Providers.PgSql; using Jellyfin.Database.Providers.SqLite; using Jellyfin.Server.Implementations.DatabaseConfiguration; using MediaBrowser.Common.Configuration; @@ -22,6 +23,7 @@ public static class ServiceCollectionExtensions private static IEnumerable DatabaseProviderTypes() { yield return typeof(SqliteDatabaseProvider); + yield return typeof(PgSqlDatabaseProvider); } private static IDictionary GetSupportedDbProviders() @@ -75,6 +77,7 @@ public static class ServiceCollectionExtensions { DatabaseType = "Jellyfin-SqLite", }; + configurationManager.SaveConfiguration("database", efCoreConfiguration); } } diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index b566b3489b..01d9dcf64d 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -37,6 +37,7 @@ + From 4ce0d498abe1c52f92805a2cb04cbcfe3218817e Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 27 Jan 2025 18:32:36 +0000 Subject: [PATCH 017/508] Added pgsql devcontainer --- .devcontainer/pgsql/Dockerfile | 8 ++++ .devcontainer/pgsql/devcontainer.json | 44 +++++++++++++++++++++ .devcontainer/pgsql/docker-compose.yaml | 51 +++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 .devcontainer/pgsql/Dockerfile create mode 100644 .devcontainer/pgsql/devcontainer.json create mode 100644 .devcontainer/pgsql/docker-compose.yaml diff --git a/.devcontainer/pgsql/Dockerfile b/.devcontainer/pgsql/Dockerfile new file mode 100644 index 0000000000..800bec76f8 --- /dev/null +++ b/.devcontainer/pgsql/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/devcontainers/dotnet:1-8.0-bookworm + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/pgsql/devcontainer.json b/.devcontainer/pgsql/devcontainer.json new file mode 100644 index 0000000000..db7f84c89c --- /dev/null +++ b/.devcontainer/pgsql/devcontainer.json @@ -0,0 +1,44 @@ +{ + "name": "Development Jellyfin Server", + "image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm", + "dockerComposeFile": "docker-compose.yml", + // restores nuget packages, installs the dotnet workloads and installs the dev https certificate + "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"", + // reads the extensions list and installs them + "postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension", + "forwardPorts": ["pgadmin:8081"], + "portsAttributes": { + "8081": { + "label": "pgAdmin", + "onAutoForward": "notify", + "requireLocalPort": true + }, + "8096": { + "label": "jellyfinapi", + "onAutoForward": "notify", + "requireLocalPort": true + } + }, + "features": { + "ghcr.io/devcontainers/features/dotnet:2": { + "version": "none", + "dotnetRuntimeVersions": "9.0", + "aspNetCoreRuntimeVersions": "9.0" + }, + "ghcr.io/devcontainers-contrib/features/apt-packages:1": { + "preserve_apt_list": false, + "packages": [ + "libfontconfig1" + ] + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "dockerDashComposeVersion": "v2" + }, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {} + }, + "hostRequirements": { + "memory": "8gb", + "cpus": 4 + } +} diff --git a/.devcontainer/pgsql/docker-compose.yaml b/.devcontainer/pgsql/docker-compose.yaml new file mode 100644 index 0000000000..88954d5644 --- /dev/null +++ b/.devcontainer/pgsql/docker-compose.yaml @@ -0,0 +1,51 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + + volumes: + - ../..:/workspaces:cached + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + + # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. + network_mode: service:pgadmin + + # Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + # user: root + + # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + + db: + image: postgres:14.3 + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: jellyfin + POSTGRES_USER: password + POSTGRES_DB: Jellyfin + pgadmin: + image: dpage/pgadmin4 + restart: unless-stopped + volumes: + - ./pgadmin:/pgadmin + - pgadmin-data:/var/lib/pgadmin + environment: + - PGADMIN_DEFAULT_EMAIL=user@domain.com + - PGADMIN_DEFAULT_PASSWORD=SuperSecret + - PGADMIN_LISTEN_PORT=8081 + - PGADMIN_SERVER_JSON_FILE=/pgadmin/servers.json + - PGADMIN_CONFIG_SERVER_MODE=False + + # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally. + # (Adding the "ports" property to this file will not forward from a Codespace.) + +volumes: + postgres-data: + pgadmin-data: From 74858042fce56eeb7af5cf991569fee0dca50775 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 27 Jan 2025 19:14:11 +0000 Subject: [PATCH 018/508] Added devcontainer for pgsql development --- .devcontainer/devcontainer.json | 2 ++ .devcontainer/pgsql/Dockerfile | 2 +- .devcontainer/pgsql/devcontainer.json | 5 +++-- .devcontainer/pgsql/docker-compose.yaml | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 84c1dda971..bcdd82cb9a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,8 @@ { "name": "Development Jellyfin Server", "image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", // restores nuget packages, installs the dotnet workloads and installs the dev https certificate "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"", // reads the extensions list and installs them diff --git a/.devcontainer/pgsql/Dockerfile b/.devcontainer/pgsql/Dockerfile index 800bec76f8..ff7f3bcd79 100644 --- a/.devcontainer/pgsql/Dockerfile +++ b/.devcontainer/pgsql/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/dotnet:1-8.0-bookworm +FROM mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm # [Optional] Uncomment this section to install additional OS packages. # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ diff --git a/.devcontainer/pgsql/devcontainer.json b/.devcontainer/pgsql/devcontainer.json index db7f84c89c..7fb09fdc9c 100644 --- a/.devcontainer/pgsql/devcontainer.json +++ b/.devcontainer/pgsql/devcontainer.json @@ -1,7 +1,8 @@ { "name": "Development Jellyfin Server", - "image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm", - "dockerComposeFile": "docker-compose.yml", + "dockerComposeFile": "docker-compose.yaml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", // restores nuget packages, installs the dotnet workloads and installs the dev https certificate "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"", // reads the extensions list and installs them diff --git a/.devcontainer/pgsql/docker-compose.yaml b/.devcontainer/pgsql/docker-compose.yaml index 88954d5644..891a03673f 100644 --- a/.devcontainer/pgsql/docker-compose.yaml +++ b/.devcontainer/pgsql/docker-compose.yaml @@ -7,7 +7,7 @@ services: dockerfile: Dockerfile volumes: - - ../..:/workspaces:cached + - ../../..:/workspaces:cached # Overrides default command so things don't shut down after the process ends. command: sleep infinity From d07e1a13b315fe6ebccf5c89cacf1c8a94b13a5f Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 27 Jan 2025 20:55:39 +0000 Subject: [PATCH 019/508] Fixed collation settings --- .devcontainer/pgsql/docker-compose.yaml | 4 ++-- .../Migrations/20250127174201_InitMigration.cs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.devcontainer/pgsql/docker-compose.yaml b/.devcontainer/pgsql/docker-compose.yaml index 891a03673f..1cab707622 100644 --- a/.devcontainer/pgsql/docker-compose.yaml +++ b/.devcontainer/pgsql/docker-compose.yaml @@ -22,13 +22,13 @@ services: # (Adding the "ports" property to this file will not forward from a Codespace.) db: - image: postgres:14.3 + image: postgres:17.2 restart: unless-stopped volumes: - postgres-data:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: jellyfin - POSTGRES_USER: password + POSTGRES_USER: jellyfin POSTGRES_DB: Jellyfin pgadmin: image: dpage/pgadmin4 diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs index f1d0d15647..01ddd5ec96 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs @@ -12,6 +12,9 @@ namespace Jellyfin.Database.Providers.PgSql.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { + // shim NOCASE collation with an undefined locale and case-insensitive matching rules. + migrationBuilder.Sql("CREATE COLLATION NOCASE (provider = icu, locale = 'und-x-icu.utf8', deterministic = false)"); + migrationBuilder.CreateTable( name: "ActivityLogs", columns: table => new From 379a104cfbeaf3c3db5988865aa2db67875a76f1 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 29 Jan 2025 20:17:50 +0000 Subject: [PATCH 020/508] Changed UserName to non-deterministic field --- .devcontainer/pgsql/docker-compose.yaml | 3 +-- Jellyfin.Api/Controllers/UserController.cs | 2 +- .../ModelConfiguration/UserConfiguration.cs | 3 +-- .../Migrations/20250127174201_InitMigration.cs | 5 +---- Jellyfin.Server.Implementations/Users/UserManager.cs | 10 ++++++++-- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.devcontainer/pgsql/docker-compose.yaml b/.devcontainer/pgsql/docker-compose.yaml index 1cab707622..7aa56dd0df 100644 --- a/.devcontainer/pgsql/docker-compose.yaml +++ b/.devcontainer/pgsql/docker-compose.yaml @@ -25,7 +25,7 @@ services: image: postgres:17.2 restart: unless-stopped volumes: - - postgres-data:/var/lib/postgresql/data + - ./pgdata/var/lib/postgresql/data environment: POSTGRES_PASSWORD: jellyfin POSTGRES_USER: jellyfin @@ -47,5 +47,4 @@ services: # (Adding the "ports" property to this file will not forward from a Codespace.) volumes: - postgres-data: pgadmin-data: diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 838578fab8..88e5d46adc 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -390,7 +390,7 @@ public class UserController : BaseJellyfinApiController return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed."); } - if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) + if (!string.Equals(user.Username, updateUser.Name, StringComparison.OrdinalIgnoreCase)) { await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false); } diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs index a369cf6562..bcaa3634ed 100644 --- a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs @@ -13,8 +13,7 @@ namespace Jellyfin.Server.Implementations.ModelConfiguration public void Configure(EntityTypeBuilder builder) { builder - .Property(user => user.Username) - .UseCollation("NOCASE"); + .Property(user => user.Username); builder .HasOne(u => u.ProfileImage) diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs index 01ddd5ec96..ac9ce3be57 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs @@ -12,9 +12,6 @@ namespace Jellyfin.Database.Providers.PgSql.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { - // shim NOCASE collation with an undefined locale and case-insensitive matching rules. - migrationBuilder.Sql("CREATE COLLATION NOCASE (provider = icu, locale = 'und-x-icu.utf8', deterministic = false)"); - migrationBuilder.CreateTable( name: "ActivityLogs", columns: table => new @@ -230,7 +227,7 @@ namespace Jellyfin.Database.Providers.PgSql.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - Username = table.Column(type: "character varying(255)", maxLength: 255, nullable: false, collation: "NOCASE"), + Username = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), Password = table.Column(type: "character varying(65535)", maxLength: 65535, nullable: true), MustUpdatePassword = table.Column(type: "boolean", nullable: false), AudioLanguagePreference = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 44de11b661..1939122eb0 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -147,7 +147,7 @@ namespace Jellyfin.Server.Implementations.Users ThrowIfInvalidUsername(newName); - if (user.Username.Equals(newName, StringComparison.Ordinal)) + if (user.Username.Equals(newName, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("The new and old names must be different."); } @@ -155,8 +155,11 @@ namespace Jellyfin.Server.Implementations.Users var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { +#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons +#pragma warning disable CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture +#pragma warning disable CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings if (await dbContext.Users - .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id)) + .AnyAsync(u => u.Username.ToUpper() == newName.ToUpper() && !u.Id.Equals(user.Id)) .ConfigureAwait(false)) { throw new ArgumentException(string.Format( @@ -164,6 +167,9 @@ namespace Jellyfin.Server.Implementations.Users "A user with the name '{0}' already exists.", newName)); } +#pragma warning restore CA1304 // The behavior of 'string.ToUpper()' could vary based on the current user's locale settings +#pragma warning restore CA1311 // Specify a culture or use an invariant version to avoid implicit dependency on current culture +#pragma warning restore CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons user.Username = newName; await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false); From ebe89c07b39702a3b8205d2070d95a5f79d3b1d2 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 2 Feb 2025 02:07:04 +0000 Subject: [PATCH 021/508] Fixed collation and pgsql container --- .devcontainer/pgsql/docker-compose.yaml | 2 +- .../Migrations/20250127174201_InitMigration.cs | 6 ++++-- .../Migrations/JellyfinDbContextModelSnapshot.cs | 3 +-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.devcontainer/pgsql/docker-compose.yaml b/.devcontainer/pgsql/docker-compose.yaml index 7aa56dd0df..dda6deda69 100644 --- a/.devcontainer/pgsql/docker-compose.yaml +++ b/.devcontainer/pgsql/docker-compose.yaml @@ -25,7 +25,7 @@ services: image: postgres:17.2 restart: unless-stopped volumes: - - ./pgdata/var/lib/postgresql/data + - ./pgdata:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: jellyfin POSTGRES_USER: jellyfin diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs index ac9ce3be57..ad1d28b136 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs @@ -964,19 +964,21 @@ namespace Jellyfin.Database.Providers.PgSql.Migrations table: "Peoples", column: "Name"); + // this was edited manually because "UserId" is a reserved name in pgsql migrationBuilder.CreateIndex( name: "IX_Permissions_UserId_Kind", table: "Permissions", columns: new[] { "UserId", "Kind" }, unique: true, - filter: "[UserId] IS NOT NULL"); + filter: "\"Permissions\".\"UserId\" IS NOT NULL"); + // this was edited manually because "UserId" is a reserved name in pgsql migrationBuilder.CreateIndex( name: "IX_Preferences_UserId_Kind", table: "Preferences", columns: new[] { "UserId", "Kind" }, unique: true, - filter: "[UserId] IS NOT NULL"); + filter: "\"Preferences\".\"UserId\" IS NOT NULL"); migrationBuilder.CreateIndex( name: "IX_UserData_ItemId_UserId_IsFavorite", diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs index cdaf257d4d..2d0c783be5 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs @@ -1273,8 +1273,7 @@ namespace Jellyfin.Database.Providers.PgSql.Migrations b.Property("Username") .IsRequired() .HasMaxLength(255) - .HasColumnType("character varying(255)") - .UseCollation("NOCASE"); + .HasColumnType("character varying(255)"); b.HasKey("Id"); From 4b57f2bdbba74093daf79dbf54b397baafba4512 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 2 Feb 2025 02:10:14 +0000 Subject: [PATCH 022/508] Fixed whitespace formatting --- .devcontainer/devcontainer.json | 2 +- .devcontainer/pgsql/devcontainer.json | 28 +++++++++++++------------ .devcontainer/pgsql/docker-compose.yaml | 2 -- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bcdd82cb9a..c2127ba5c3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ "name": "Development Jellyfin Server", "image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm", "service": "app", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", // restores nuget packages, installs the dotnet workloads and installs the dev https certificate "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"", // reads the extensions list and installs them diff --git a/.devcontainer/pgsql/devcontainer.json b/.devcontainer/pgsql/devcontainer.json index 7fb09fdc9c..3dd91d9755 100644 --- a/.devcontainer/pgsql/devcontainer.json +++ b/.devcontainer/pgsql/devcontainer.json @@ -2,24 +2,26 @@ "name": "Development Jellyfin Server", "dockerComposeFile": "docker-compose.yaml", "service": "app", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", // restores nuget packages, installs the dotnet workloads and installs the dev https certificate "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"", // reads the extensions list and installs them "postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension", - "forwardPorts": ["pgadmin:8081"], - "portsAttributes": { - "8081": { - "label": "pgAdmin", - "onAutoForward": "notify", - "requireLocalPort": true - }, + "forwardPorts": [ + "pgadmin:8081" + ], + "portsAttributes": { + "8081": { + "label": "pgAdmin", + "onAutoForward": "notify", + "requireLocalPort": true + }, "8096": { - "label": "jellyfinapi", - "onAutoForward": "notify", - "requireLocalPort": true - } - }, + "label": "jellyfinapi", + "onAutoForward": "notify", + "requireLocalPort": true + } + }, "features": { "ghcr.io/devcontainers/features/dotnet:2": { "version": "none", diff --git a/.devcontainer/pgsql/docker-compose.yaml b/.devcontainer/pgsql/docker-compose.yaml index dda6deda69..45af0b33e1 100644 --- a/.devcontainer/pgsql/docker-compose.yaml +++ b/.devcontainer/pgsql/docker-compose.yaml @@ -14,7 +14,6 @@ services: # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. network_mode: service:pgadmin - # Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. # user: root @@ -42,7 +41,6 @@ services: - PGADMIN_LISTEN_PORT=8081 - PGADMIN_SERVER_JSON_FILE=/pgadmin/servers.json - PGADMIN_CONFIG_SERVER_MODE=False - # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally. # (Adding the "ports" property to this file will not forward from a Codespace.) From 2e5ff6842af11c4e8ba70a6df83e1676ba1bb5e0 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 2 Feb 2025 02:13:37 +0000 Subject: [PATCH 023/508] Added collation migration in SqLite --- .../20250202021306_FixedCollation.Designer.cs | 1594 +++++++++++++++++ .../20250202021306_FixedCollation.cs | 40 + .../Migrations/JellyfinDbModelSnapshot.cs | 5 +- 3 files changed, 1636 insertions(+), 3 deletions(-) create mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.Designer.cs create mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.Designer.cs new file mode 100644 index 0000000000..d7b806d7a3 --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.Designer.cs @@ -0,0 +1,1594 @@ +// +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20250202021306_FixedCollation")] + partial class FixedCollation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.1"); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue") + .IsUnique(); + + b.ToTable("ItemValues"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "UserId", "CustomDataKey"); + + b.HasIndex("UserId"); + + b.HasIndex("ItemId", "UserId", "IsFavorite"); + + b.HasIndex("ItemId", "UserId", "LastPlayedDate"); + + b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("ItemId", "UserId", "Played"); + + b.ToTable("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Children") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem") + .WithMany("ParentAncestors") + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.HasOne("Jellyfin.Data.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") + .WithMany("UserData") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Navigation("Chapters"); + + b.Navigation("Children"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("ParentAncestors"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.cs new file mode 100644 index 0000000000..e82575e418 --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class FixedCollation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Username", + table: "Users", + type: "TEXT", + maxLength: 255, + nullable: false, + oldClrType: typeof(string), + oldType: "TEXT", + oldMaxLength: 255, + oldCollation: "NOCASE"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Username", + table: "Users", + type: "TEXT", + maxLength: 255, + nullable: false, + collation: "NOCASE", + oldClrType: typeof(string), + oldType: "TEXT", + oldMaxLength: 255); + } + } +} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs index e75760d805..ddcc37195d 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + modelBuilder.HasAnnotation("ProductVersion", "9.0.1"); modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => { @@ -1244,8 +1244,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("Username") .IsRequired() .HasMaxLength(255) - .HasColumnType("TEXT") - .UseCollation("NOCASE"); + .HasColumnType("TEXT"); b.HasKey("Id"); From 61b2ad7f496b6e47ed3646636f57d96916ffbf33 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 2 Feb 2025 02:21:34 +0000 Subject: [PATCH 024/508] Added missing assembly info and fixed migration tests --- .../Properties/AssemblyInfo.cs | 23 ++++++++++++++++++ .../Jellyfin.Database.Providers.SqLite.csproj | 4 ++++ .../Properties/AssemblyInfo.cs | 23 ++++++++++++++++++ .../EfMigrations/EfMigrationTests.cs | 24 +++++++++++++------ ...llyfin.Server.Implementations.Tests.csproj | 3 +++ 5 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Properties/AssemblyInfo.cs create mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Properties/AssemblyInfo.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Properties/AssemblyInfo.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..27022c7de8 --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Properties/AssemblyInfo.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Jellyfin.Database.Providers.PgSql")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Jellyfin Project")] +[assembly: AssemblyProduct("Jellyfin Server")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] +[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj index e77c944f95..fca872d902 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj @@ -48,4 +48,8 @@ + + + + diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Properties/AssemblyInfo.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..8d6920f2e8 --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Properties/AssemblyInfo.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Jellyfin.Database.Providers.SqLite")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Jellyfin Project")] +[assembly: AssemblyProduct("Jellyfin Server")] +[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] +[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] diff --git a/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs b/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs index 54d5d2adf8..ab388eca86 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs @@ -1,5 +1,7 @@ using System; using System.Threading.Tasks; +using Jellyfin.Database.Providers.PgSql; +using Jellyfin.Server.Implementations.Migrations; using Microsoft.EntityFrameworkCore; using Xunit; @@ -7,11 +9,19 @@ namespace Jellyfin.Server.Implementations.Tests.EfMigrations; public class EfMigrationTests { - // [Fact] - // public void CheckForUnappliedMigrations() - // { - // var dbDesignContext = new DesignTimeJellyfinDbFactory(); - // var context = dbDesignContext.CreateDbContext([]); - // Assert.False(context.Database.HasPendingModelChanges(), "There are unapplied changes to the EfCore model. Please create a Migration."); - // } + [Fact] + public void CheckForUnappliedMigrations_PgSQL() + { + var dbDesignContext = new PgSqlDesignTimeJellyfinDbFactory(); + var context = dbDesignContext.CreateDbContext([]); + Assert.False(context.Database.HasPendingModelChanges(), "There are unapplied changes to the EfCore model for PgSQL. Please create a Migration."); + } + + [Fact] + public void CheckForUnappliedMigrations_SqLite() + { + var dbDesignContext = new SqliteDesignTimeJellyfinDbFactory(); + var context = dbDesignContext.CreateDbContext([]); + Assert.False(context.Database.HasPendingModelChanges(), "There are unapplied changes to the EfCore model for PgSQL. Please create a Migration."); + } } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 4f018ba694..84a3951f67 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -29,6 +29,9 @@ + + + From efb402b1d27ca3f3444f7b81553ced804029bb3f Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 2 Feb 2025 02:32:28 +0000 Subject: [PATCH 025/508] Fixed shutdown behavior --- .../IJellyfinDatabaseProvider.cs | 9 ++++++++- .../PgSqlDatabaseProvider.cs | 4 ++-- .../SqliteDatabaseProvider.cs | 6 +++--- Jellyfin.Server/Program.cs | 6 ++++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs index 72a6f819e0..b27a88971d 100644 --- a/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs @@ -8,7 +8,7 @@ namespace Jellyfin.Server.Implementations; /// /// Defines the type and extension points for multi database support. /// -public interface IJellyfinDatabaseProvider : IAsyncDisposable +public interface IJellyfinDatabaseProvider { /// /// Gets or Sets the Database Factory when initialisaition is done. @@ -33,4 +33,11 @@ public interface IJellyfinDatabaseProvider : IAsyncDisposable /// The token to abort the operation. /// A representing the asynchronous operation. Task RunScheduledOptimisation(CancellationToken cancellationToken); + + /// + /// If supported this should perform any actions that are required on stopping the jellyfin server. + /// + /// The token that will be used to abort the operation. + /// A representing the asynchronous operation. + Task RunShutdownTask(CancellationToken cancellationToken); } diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs index 1dae3401bc..e6ae2acea7 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs @@ -68,8 +68,8 @@ public sealed class PgSqlDatabaseProvider : IJellyfinDatabaseProvider } /// - public ValueTask DisposeAsync() + public Task RunShutdownTask(CancellationToken cancellationToken) { - return ValueTask.CompletedTask; + return Task.CompletedTask; } } diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs index 8ef5b6af5e..907ea91561 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs @@ -64,13 +64,13 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider } /// - public async ValueTask DisposeAsync() + public async Task RunShutdownTask(CancellationToken cancellationToken) { // Run before disposing the application - var context = await DbContextFactory!.CreateDbContextAsync().ConfigureAwait(false); + var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { - await context.Database.ExecuteSqlRawAsync("PRAGMA optimize").ConfigureAwait(false); + await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false); } SqliteConnection.ClearAllPools(); diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index fd23b7e25c..bb93ef1385 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; @@ -197,8 +198,9 @@ namespace Jellyfin.Server _logger.LogInformation("Running query planner optimizations in the database... This might take a while"); var databaseProvider = appHost.ServiceProvider.GetRequiredService(); - - await databaseProvider.DisposeAsync().ConfigureAwait(false); + var shutdownSource = new CancellationTokenSource(); + shutdownSource.CancelAfter((int)TimeSpan.FromSeconds(60).TotalMicroseconds); + await databaseProvider.RunShutdownTask(shutdownSource.Token).ConfigureAwait(false); } host?.Dispose(); From c9237ae73149bc8c8d18d94557b4927deb06175b Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 3 Feb 2025 20:15:36 +0000 Subject: [PATCH 026/508] Applied review suggestions --- .../PgSqlDatabaseProvider.cs | 2 +- .../SqliteDatabaseProvider.cs | 4 ++-- Jellyfin.Database/readme.md | 4 ++-- .../Extensions/ServiceCollectionExtensions.cs | 4 ++-- .../EfMigrations/EfMigrationTests.cs | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs index e6ae2acea7..021110742e 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs @@ -9,7 +9,7 @@ using Npgsql; namespace Jellyfin.Database.Providers.PgSql; /// -/// Configures jellyfin to use an SqLite database. +/// Configures jellyfin to use an Postgres database. /// [JellyfinDatabaseProviderKey("Jellyfin-PgSql")] public sealed class PgSqlDatabaseProvider : IJellyfinDatabaseProvider diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs index 907ea91561..ef4f00384b 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs @@ -8,9 +8,9 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Database.Providers.SqLite; /// -/// Configures jellyfin to use an SqLite database. +/// Configures jellyfin to use an SQLite database. /// -[JellyfinDatabaseProviderKey("Jellyfin-SqLite")] +[JellyfinDatabaseProviderKey("Jellyfin-SQLite")] public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider { private readonly IApplicationPaths _applicationPaths; diff --git a/Jellyfin.Database/readme.md b/Jellyfin.Database/readme.md index 883aff2d75..55d57d3eff 100644 --- a/Jellyfin.Database/readme.md +++ b/Jellyfin.Database/readme.md @@ -12,12 +12,12 @@ When creating a new migration, you always have to create migrations for all prov dotnet ef migrations add MIGRATION_NAME --project "PATH_TO_PROJECT" -- --provider PROVIDER_KEY ``` -with both sqlite and pgsql currently beeing supported and both need migrations, you need to run the efcore tool with the correct project to tell EfCore where to store the migrations and the correct provider key to tell jellyfin to load that provider. +with both sqlite and pgsql currently beeing supported and both need migrations, you need to run the Entity Framework tool with the correct project to tell EFCore where to store the migrations and the correct provider key to tell jellyfin to load that provider. The example is made from the root folder of the project e.g for codespaces `/workspaces/jellyfin` ```cmd -dotnet ef migrations add {MIGRATION_NAME} --project "Jellyfin.Database/Jellyfin.Database.Providers.SqLite" -- --migration-provider Jellyfin-SqLite +dotnet ef migrations add {MIGRATION_NAME} --project "Jellyfin.Database/Jellyfin.Database.Providers.SqLite" -- --migration-provider Jellyfin-SQLite dotnet ef migrations add {MIGRATION_NAME} --project "Jellyfin.Database/Jellyfin.Database.Providers.PgSql" -- --migration-provider Jellyfin-PgSql ``` diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index 7936c6fd98..730e628df2 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -72,10 +72,10 @@ public static class ServiceCollectionExtensions } else { - // when nothing is setup via new Database configuration, fallback to SqLite with default settings. + // when nothing is setup via new Database configuration, fallback to SQLite with default settings. efCoreConfiguration = new DatabaseConfigurationOptions() { - DatabaseType = "Jellyfin-SqLite", + DatabaseType = "Jellyfin-SQLite", }; configurationManager.SaveConfiguration("database", efCoreConfiguration); } diff --git a/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs b/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs index ab388eca86..78e431be14 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs @@ -14,7 +14,7 @@ public class EfMigrationTests { var dbDesignContext = new PgSqlDesignTimeJellyfinDbFactory(); var context = dbDesignContext.CreateDbContext([]); - Assert.False(context.Database.HasPendingModelChanges(), "There are unapplied changes to the EfCore model for PgSQL. Please create a Migration."); + Assert.False(context.Database.HasPendingModelChanges(), "There are unapplied changes to the EFCore model for PgSQL. Please create a Migration."); } [Fact] @@ -22,6 +22,6 @@ public class EfMigrationTests { var dbDesignContext = new SqliteDesignTimeJellyfinDbFactory(); var context = dbDesignContext.CreateDbContext([]); - Assert.False(context.Database.HasPendingModelChanges(), "There are unapplied changes to the EfCore model for PgSQL. Please create a Migration."); + Assert.False(context.Database.HasPendingModelChanges(), "There are unapplied changes to the EFCore model for SQLite. Please create a Migration."); } } From df8f352d6543755d4cdc5e8c168c3032600f85cb Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 3 Feb 2025 20:16:58 +0000 Subject: [PATCH 027/508] Made key lookup case insensitive --- .../Extensions/ServiceCollectionExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index 730e628df2..1dd7cfe045 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -38,7 +38,7 @@ public static class ServiceCollectionExtensions } var provider = providerType; - items[keyAttribute.DatabaseProviderKey] = (services) => (IJellyfinDatabaseProvider)ActivatorUtilities.CreateInstance(services, providerType); + items[keyAttribute.DatabaseProviderKey.ToUpperInvariant()] = (services) => (IJellyfinDatabaseProvider)ActivatorUtilities.CreateInstance(services, providerType); } return items; @@ -81,7 +81,7 @@ public static class ServiceCollectionExtensions } } - if (!providers.TryGetValue(efCoreConfiguration.DatabaseType, out providerFactory!)) + if (!providers.TryGetValue(efCoreConfiguration.DatabaseType.ToUpperInvariant(), out providerFactory!)) { throw new InvalidOperationException($"Jellyfin cannot find the database provider of type '{efCoreConfiguration.DatabaseType}'. Supported types are {string.Join(", ", providers.Keys)}"); } From 078587d232ccbfdf6e08c6f1a6435e4e397e4bdc Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 3 Feb 2025 20:20:37 +0000 Subject: [PATCH 028/508] Added Version string to application name connection for pgsql --- .../PgSqlDatabaseProvider.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs index 021110742e..ccaf38d2ac 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs @@ -1,4 +1,6 @@ using System; +using System.Diagnostics; +using System.Reflection; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.DatabaseConfiguration; using MediaBrowser.Common.Configuration; @@ -42,7 +44,7 @@ public sealed class PgSqlDatabaseProvider : IJellyfinDatabaseProvider } var connectionBuilder = new NpgsqlConnectionStringBuilder(); - connectionBuilder.ApplicationName = "jellyfin"; + connectionBuilder.ApplicationName = $"jellyfin+{FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).FileVersion}"; connectionBuilder.CommandTimeout = dbSettings.PostgreSql.Timeout; connectionBuilder.Database = dbSettings.PostgreSql.DatabaseName; connectionBuilder.Username = dbSettings.PostgreSql.Username; From b0e853070ba368099fdc78d925d4d5675eb00d64 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 4 Feb 2025 15:38:25 +0800 Subject: [PATCH 029/508] Don't use RETURNING clause with EFCore The RETURNING clause helps with performance and is now default of EFCore. However, EFCore cannot automatically perform retry when the table was locked/busy. Disable it as a workaround for the locking issues of very huge databases. --- .../JellyfinDbContext.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs index 34d9e3960d..43ea2bd3c2 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs +++ b/Jellyfin.Server.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; @@ -271,4 +273,23 @@ public class JellyfinDbContext(DbContextOptions options, ILog // Configuration for each entity is in its own class inside 'ModelConfiguration'. modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDbContext).Assembly); } + + /// + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + configurationBuilder.Conventions.Add(_ => new DoNotUseReturningClauseConvention()); + } + + private class DoNotUseReturningClauseConvention : IModelFinalizingConvention + { + public void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + entityType.UseSqlReturningClause(false); + } + } + } } From 533ceeaaf299c9396f82e11e16314afbc03407f8 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 4 Feb 2025 16:52:17 +0800 Subject: [PATCH 030/508] Fix subnet contains check We are still using `Subnet.Contains` a lot but that does not handle IPv4 mapped to IPv6 addresses at all. It was partially fixed by #12094 in local network checking, but it may not always happen on LAN. Also make all local network checking to use IsInLocalNetwork method instead of just performing `Subnet.Contains` which is not accurate. Filter out all link-local addresses for external interface matching. --- MediaBrowser.Common/Net/NetworkUtils.cs | 19 +++++++++ .../Manager/NetworkManager.cs | 42 +++++++++---------- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index 7380963520..e21fdeb3d4 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -326,4 +326,23 @@ public static partial class NetworkUtils return new IPAddress(BitConverter.GetBytes(broadCastIPAddress)); } + + /// + /// Check if a subnet contains an address. This method also handles IPv4 mapped to IPv6 addresses. + /// + /// The . + /// The . + /// Whether the supplied IP is in the supplied network. + public static bool SubNetContainsAddress(IPNetwork network, IPAddress address) + { + ArgumentNullException.ThrowIfNull(address); + ArgumentNullException.ThrowIfNull(network); + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + return network.Contains(address); + } } diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index dd01e9533b..15b3edbd59 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -689,10 +689,10 @@ public class NetworkManager : INetworkManager, IDisposable { // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely. // If left blank, all remote addresses will be allowed. - if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIP))) + if (_remoteAddressFilter.Any() && !IsInLocalNetwork(remoteIP)) { // remoteAddressFilter is a whitelist or blacklist. - var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP)); + var matches = _remoteAddressFilter.Count(remoteNetwork => NetworkUtils.SubNetContainsAddress(remoteNetwork, remoteIP)); if ((!config.IsRemoteIPFilterBlacklist && matches > 0) || (config.IsRemoteIPFilterBlacklist && matches == 0)) { @@ -816,7 +816,7 @@ public class NetworkManager : INetworkManager, IDisposable _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); } - bool isExternal = !_lanSubnets.Any(network => network.Contains(source)); + bool isExternal = !IsInLocalNetwork(source); _logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal); if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result)) @@ -863,7 +863,7 @@ public class NetworkManager : INetworkManager, IDisposable // (For systems with multiple internal network cards, and multiple subnets) foreach (var intf in availableInterfaces) { - if (intf.Subnet.Contains(source)) + if (NetworkUtils.SubNetContainsAddress(intf.Subnet, source)) { result = NetworkUtils.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result); @@ -891,21 +891,11 @@ public class NetworkManager : INetworkManager, IDisposable { if (NetworkUtils.TryParseToSubnet(address, out var subnet)) { - return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix))); + return IsInLocalNetwork(subnet.Prefix); } - if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) - { - foreach (var ept in addresses) - { - if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept)))) - { - return true; - } - } - } - - return false; + return NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled) + && addresses.Any(IsInLocalNetwork); } /// @@ -940,6 +930,11 @@ public class NetworkManager : INetworkManager, IDisposable return CheckIfLanAndNotExcluded(address); } + /// + /// Check if the address is in the LAN and not excluded. + /// + /// The IP address to check. The caller should make sure this is not an IPv4MappedToIPv6 address. + /// Boolean indicates whether the address is in LAN. private bool CheckIfLanAndNotExcluded(IPAddress address) { foreach (var lanSubnet in _lanSubnets) @@ -979,7 +974,7 @@ public class NetworkManager : INetworkManager, IDisposable { // Only use matching internal subnets // Prefer more specific (bigger subnet prefix) overrides - validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source)) + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && NetworkUtils.SubNetContainsAddress(x.Data.Subnet, source)) .OrderByDescending(x => x.Data.Subnet.PrefixLength) .ToList(); } @@ -987,7 +982,7 @@ public class NetworkManager : INetworkManager, IDisposable { // Only use matching external subnets // Prefer more specific (bigger subnet prefix) overrides - validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source)) + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && NetworkUtils.SubNetContainsAddress(x.Data.Subnet, source)) .OrderByDescending(x => x.Data.Subnet.PrefixLength) .ToList(); } @@ -995,7 +990,7 @@ public class NetworkManager : INetworkManager, IDisposable foreach (var data in validPublishedServerUrls) { // Get interface matching override subnet - var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address)); + var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => NetworkUtils.SubNetContainsAddress(data.Data.Subnet, x.Address)); if (intf?.Address is not null || (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any)) @@ -1058,6 +1053,7 @@ public class NetworkManager : INetworkManager, IDisposable if (isInExternalSubnet) { var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address)) + .Where(x => !IsLinkLocalAddress(x.Address)) .OrderBy(x => x.Index) .ToList(); if (externalInterfaces.Count > 0) @@ -1065,7 +1061,7 @@ public class NetworkManager : INetworkManager, IDisposable // Check to see if any of the external bind interfaces are in the same subnet as the source. // If none exists, this will select the first external interface if there is one. bindAddress = externalInterfaces - .OrderByDescending(x => x.Subnet.Contains(source)) + .OrderByDescending(x => NetworkUtils.SubNetContainsAddress(x.Subnet, source)) .ThenByDescending(x => x.Subnet.PrefixLength) .ThenBy(x => x.Index) .Select(x => x.Address) @@ -1083,7 +1079,7 @@ public class NetworkManager : INetworkManager, IDisposable // Check to see if any of the internal bind interfaces are in the same subnet as the source. // If none exists, this will select the first internal interface if there is one. bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address)) - .OrderByDescending(x => x.Subnet.Contains(source)) + .OrderByDescending(x => NetworkUtils.SubNetContainsAddress(x.Subnet, source)) .ThenByDescending(x => x.Subnet.PrefixLength) .ThenBy(x => x.Index) .Select(x => x.Address) @@ -1127,7 +1123,7 @@ public class NetworkManager : INetworkManager, IDisposable // (For systems with multiple network cards and/or multiple subnets) foreach (var intf in extResult) { - if (intf.Subnet.Contains(source)) + if (NetworkUtils.SubNetContainsAddress(intf.Subnet, source)) { result = NetworkUtils.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result); From 9aec576c763eeb6a2d1538175d397ab933227e5a Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 5 Feb 2025 08:04:29 +0800 Subject: [PATCH 031/508] Typo Co-authored-by: Cody Robibero --- MediaBrowser.Common/Net/NetworkUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index e21fdeb3d4..a498d6271b 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -333,7 +333,7 @@ public static partial class NetworkUtils /// The . /// The . /// Whether the supplied IP is in the supplied network. - public static bool SubNetContainsAddress(IPNetwork network, IPAddress address) + public static bool SubnetContainsAddress(IPNetwork network, IPAddress address) { ArgumentNullException.ThrowIfNull(address); ArgumentNullException.ThrowIfNull(network); From 4e64b261a8a6f3b02a82d295b347755e46ad1cc2 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 5 Feb 2025 18:13:28 -0600 Subject: [PATCH 032/508] Moved Trimmed to Jellyfin.Extensions.StringExtensions --- MediaBrowser.Controller/Sorting/SortExtensions.cs | 5 ----- MediaBrowser.Providers/MediaInfo/AudioFileProber.cs | 1 + MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs | 2 +- MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs | 2 +- MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs | 1 + src/Jellyfin.Extensions/StringExtensions.cs | 12 ++++++++++++ 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs index db934e0f47..f9c0d39ddd 100644 --- a/MediaBrowser.Controller/Sorting/SortExtensions.cs +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -30,10 +30,5 @@ namespace MediaBrowser.Controller.Sorting { return list.ThenByDescending(getName, _comparer); } - - public static IEnumerable Trimmed(this IEnumerable values) - { - return values.Select(i => (i ?? string.Empty).Trim()); - } } } diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 1308e06f9a..0e22dd96ed 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using ATL; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 228c51959d..266e1861f9 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; @@ -16,7 +17,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs index 4cb6f81b73..774539c95e 100644 --- a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs @@ -4,11 +4,11 @@ using System.Globalization; using System.IO; using System.Linq; using System.Xml; +using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index 2ff2fc716f..d119751791 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml; +using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs index 4b9677d9f4..715cbf2209 100644 --- a/src/Jellyfin.Extensions/StringExtensions.cs +++ b/src/Jellyfin.Extensions/StringExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; using ICU4N.Text; @@ -123,5 +125,15 @@ namespace Jellyfin.Extensions { return (_transliterator.Value is null) ? text : _transliterator.Value.Transliterate(text); } + + /// + /// Ensures all strings are non-null and trimmed of leading an trailing blanks. + /// + /// The enumerable of strings to trim. + /// The enumeration of trimmed strings. + public static IEnumerable Trimmed(this IEnumerable values) + { + return values.Select(i => (i ?? string.Empty).Trim()); + } } } From 8b07c1f53ddf491499005b9e0da9cf1bbe93cf9a Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 19 Feb 2025 15:38:32 +0000 Subject: [PATCH 033/508] Fixed gitignore for pgsql data --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index d5a0367bff..fe312b647c 100644 --- a/.gitignore +++ b/.gitignore @@ -277,3 +277,5 @@ apiclient/generated # Omnisharp crash logs mono_crash.*.json + +.devcontainer/pgsql/pgdata \ No newline at end of file From ddc20b74bf2eddd686c14c084260ca457011f8be Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 19 Feb 2025 18:21:23 +0000 Subject: [PATCH 034/508] Removed pgsql from refactor --- .../Jellyfin.Database.Providers.PgSql.csproj | 50 - .../20250127174201_InitMigration.Designer.cs | 1624 ----------------- .../20250127174201_InitMigration.cs | 1106 ----------- ...83152_MakeStartEndDateNullable.Designer.cs | 1623 ---------------- ...20250205183152_MakeStartEndDateNullable.cs | 55 - .../JellyfinDbContextModelSnapshot.cs | 1620 ---------------- .../PgSqlDesignTimeJellyfinDbFactory.cs | 25 - .../PgSqlDatabaseProvider.cs | 77 - .../Properties/AssemblyInfo.cs | 23 - .../Extensions/ServiceCollectionExtensions.cs | 4 - Jellyfin.sln | 9 +- .../EfMigrations/EfMigrationTests.cs | 11 - 12 files changed, 1 insertion(+), 6226 deletions(-) delete mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Jellyfin.Database.Providers.PgSql.csproj delete mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.Designer.cs delete mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs delete mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.Designer.cs delete mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.cs delete mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs delete mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/PgSqlDesignTimeJellyfinDbFactory.cs delete mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs delete mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Properties/AssemblyInfo.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Jellyfin.Database.Providers.PgSql.csproj b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Jellyfin.Database.Providers.PgSql.csproj deleted file mode 100644 index 785a3c63ab..0000000000 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Jellyfin.Database.Providers.PgSql.csproj +++ /dev/null @@ -1,50 +0,0 @@ - - - - net9.0 - enable - enable - false - true - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.Designer.cs deleted file mode 100644 index 47fff07214..0000000000 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.Designer.cs +++ /dev/null @@ -1,1624 +0,0 @@ -// -using System; -using Jellyfin.Server.Implementations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Jellyfin.Database.Providers.PgSql.Migrations -{ - [DbContext(typeof(JellyfinDbContext))] - [Migration("20250127174201_InitMigration")] - partial class InitMigration - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.1") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DayOfWeek") - .HasColumnType("integer"); - - b.Property("EndHour") - .HasColumnType("double precision"); - - b.Property("StartHour") - .HasColumnType("double precision"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AccessSchedules"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("ItemId") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LogSeverity") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Overview") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("bigint"); - - b.Property("ShortOverview") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("DateCreated"); - - b.ToTable("ActivityLogs"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("ParentItemId") - .HasColumnType("uuid"); - - b.HasKey("ItemId", "ParentItemId"); - - b.HasIndex("ParentItemId"); - - b.ToTable("AncestorIds"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("Index") - .HasColumnType("integer"); - - b.Property("Codec") - .IsRequired() - .HasColumnType("text"); - - b.Property("CodecTag") - .HasColumnType("text"); - - b.Property("Comment") - .HasColumnType("text"); - - b.Property("Filename") - .HasColumnType("text"); - - b.Property("MimeType") - .HasColumnType("text"); - - b.HasKey("ItemId", "Index"); - - b.ToTable("AttachmentStreamInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Album") - .HasColumnType("text"); - - b.Property("AlbumArtists") - .HasColumnType("text"); - - b.Property("Artists") - .HasColumnType("text"); - - b.Property("Audio") - .HasColumnType("integer"); - - b.Property("ChannelId") - .HasColumnType("text"); - - b.Property("CleanName") - .HasColumnType("text"); - - b.Property("CommunityRating") - .HasColumnType("real"); - - b.Property("CriticRating") - .HasColumnType("real"); - - b.Property("CustomRating") - .HasColumnType("text"); - - b.Property("Data") - .HasColumnType("text"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastMediaAdded") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastRefreshed") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastSaved") - .HasColumnType("timestamp with time zone"); - - b.Property("DateModified") - .HasColumnType("timestamp with time zone"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("EpisodeTitle") - .HasColumnType("text"); - - b.Property("ExternalId") - .HasColumnType("text"); - - b.Property("ExternalSeriesId") - .HasColumnType("text"); - - b.Property("ExternalServiceId") - .HasColumnType("text"); - - b.Property("ExtraIds") - .HasColumnType("text"); - - b.Property("ExtraType") - .HasColumnType("integer"); - - b.Property("ForcedSortName") - .HasColumnType("text"); - - b.Property("Genres") - .HasColumnType("text"); - - b.Property("Height") - .HasColumnType("integer"); - - b.Property("IndexNumber") - .HasColumnType("integer"); - - b.Property("InheritedParentalRatingValue") - .HasColumnType("integer"); - - b.Property("IsFolder") - .HasColumnType("boolean"); - - b.Property("IsInMixedFolder") - .HasColumnType("boolean"); - - b.Property("IsLocked") - .HasColumnType("boolean"); - - b.Property("IsMovie") - .HasColumnType("boolean"); - - b.Property("IsRepeat") - .HasColumnType("boolean"); - - b.Property("IsSeries") - .HasColumnType("boolean"); - - b.Property("IsVirtualItem") - .HasColumnType("boolean"); - - b.Property("LUFS") - .HasColumnType("real"); - - b.Property("MediaType") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("NormalizationGain") - .HasColumnType("real"); - - b.Property("OfficialRating") - .HasColumnType("text"); - - b.Property("OriginalTitle") - .HasColumnType("text"); - - b.Property("Overview") - .HasColumnType("text"); - - b.Property("OwnerId") - .HasColumnType("text"); - - b.Property("ParentId") - .HasColumnType("uuid"); - - b.Property("ParentIndexNumber") - .HasColumnType("integer"); - - b.Property("Path") - .HasColumnType("text"); - - b.Property("PreferredMetadataCountryCode") - .HasColumnType("text"); - - b.Property("PreferredMetadataLanguage") - .HasColumnType("text"); - - b.Property("PremiereDate") - .HasColumnType("timestamp with time zone"); - - b.Property("PresentationUniqueKey") - .HasColumnType("text"); - - b.Property("PrimaryVersionId") - .HasColumnType("text"); - - b.Property("ProductionLocations") - .HasColumnType("text"); - - b.Property("ProductionYear") - .HasColumnType("integer"); - - b.Property("RunTimeTicks") - .HasColumnType("bigint"); - - b.Property("SeasonId") - .HasColumnType("uuid"); - - b.Property("SeasonName") - .HasColumnType("text"); - - b.Property("SeriesId") - .HasColumnType("uuid"); - - b.Property("SeriesName") - .HasColumnType("text"); - - b.Property("SeriesPresentationUniqueKey") - .HasColumnType("text"); - - b.Property("ShowId") - .HasColumnType("text"); - - b.Property("Size") - .HasColumnType("bigint"); - - b.Property("SortName") - .HasColumnType("text"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Studios") - .HasColumnType("text"); - - b.Property("Tagline") - .HasColumnType("text"); - - b.Property("Tags") - .HasColumnType("text"); - - b.Property("TopParentId") - .HasColumnType("uuid"); - - b.Property("TotalBitrate") - .HasColumnType("integer"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UnratedType") - .HasColumnType("text"); - - b.Property("Width") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ParentId"); - - b.HasIndex("Path"); - - b.HasIndex("PresentationUniqueKey"); - - b.HasIndex("TopParentId", "Id"); - - b.HasIndex("Type", "TopParentId", "Id"); - - b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); - - b.HasIndex("Type", "TopParentId", "StartDate"); - - b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); - - b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); - - b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); - - b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); - - b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); - - b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); - - b.ToTable("BaseItems"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Blurhash") - .HasColumnType("bytea"); - - b.Property("DateModified") - .HasColumnType("timestamp with time zone"); - - b.Property("Height") - .HasColumnType("integer"); - - b.Property("ImageType") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("Path") - .IsRequired() - .HasColumnType("text"); - - b.Property("Width") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ItemId"); - - b.ToTable("BaseItemImageInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => - { - b.Property("Id") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.HasKey("Id", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("BaseItemMetadataFields"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("ProviderId") - .HasColumnType("text"); - - b.Property("ProviderValue") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ItemId", "ProviderId"); - - b.HasIndex("ProviderId", "ProviderValue", "ItemId"); - - b.ToTable("BaseItemProviders"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => - { - b.Property("Id") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.HasKey("Id", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("BaseItemTrailerTypes"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("ChapterIndex") - .HasColumnType("integer"); - - b.Property("ImageDateModified") - .HasColumnType("timestamp with time zone"); - - b.Property("ImagePath") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("StartPositionTicks") - .HasColumnType("bigint"); - - b.HasKey("ItemId", "ChapterIndex"); - - b.ToTable("Chapters"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("Value") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client", "Key") - .IsUnique(); - - b.ToTable("CustomItemDisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChromecastVersion") - .HasColumnType("integer"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DashboardTheme") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("EnableNextVideoInfoOverlay") - .HasColumnType("boolean"); - - b.Property("IndexBy") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("ScrollDirection") - .HasColumnType("integer"); - - b.Property("ShowBackdrop") - .HasColumnType("boolean"); - - b.Property("ShowSidebar") - .HasColumnType("boolean"); - - b.Property("SkipBackwardLength") - .HasColumnType("integer"); - - b.Property("SkipForwardLength") - .HasColumnType("integer"); - - b.Property("TvHome") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client") - .IsUnique(); - - b.ToTable("DisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DisplayPreferencesId") - .HasColumnType("integer"); - - b.Property("Order") - .HasColumnType("integer"); - - b.Property("Type") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("DisplayPreferencesId"); - - b.ToTable("HomeSection"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("LastModified") - .HasColumnType("timestamp with time zone"); - - b.Property("Path") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("ImageInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("IndexBy") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("RememberIndexing") - .HasColumnType("boolean"); - - b.Property("RememberSorting") - .HasColumnType("boolean"); - - b.Property("SortBy") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("ViewType") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("ItemDisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => - { - b.Property("ItemValueId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CleanValue") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .HasColumnType("integer"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ItemValueId"); - - b.HasIndex("Type", "CleanValue") - .IsUnique(); - - b.ToTable("ItemValues"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => - { - b.Property("ItemValueId") - .HasColumnType("uuid"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.HasKey("ItemValueId", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("ItemValuesMap"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EndTicks") - .HasColumnType("bigint"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("SegmentProviderId") - .IsRequired() - .HasColumnType("text"); - - b.Property("StartTicks") - .HasColumnType("bigint"); - - b.Property("Type") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.ToTable("MediaSegments"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("StreamIndex") - .HasColumnType("integer"); - - b.Property("AspectRatio") - .HasColumnType("text"); - - b.Property("AverageFrameRate") - .HasColumnType("real"); - - b.Property("BitDepth") - .HasColumnType("integer"); - - b.Property("BitRate") - .HasColumnType("integer"); - - b.Property("BlPresentFlag") - .HasColumnType("integer"); - - b.Property("ChannelLayout") - .HasColumnType("text"); - - b.Property("Channels") - .HasColumnType("integer"); - - b.Property("Codec") - .HasColumnType("text"); - - b.Property("CodecTag") - .HasColumnType("text"); - - b.Property("CodecTimeBase") - .HasColumnType("text"); - - b.Property("ColorPrimaries") - .HasColumnType("text"); - - b.Property("ColorSpace") - .HasColumnType("text"); - - b.Property("ColorTransfer") - .HasColumnType("text"); - - b.Property("Comment") - .HasColumnType("text"); - - b.Property("DvBlSignalCompatibilityId") - .HasColumnType("integer"); - - b.Property("DvLevel") - .HasColumnType("integer"); - - b.Property("DvProfile") - .HasColumnType("integer"); - - b.Property("DvVersionMajor") - .HasColumnType("integer"); - - b.Property("DvVersionMinor") - .HasColumnType("integer"); - - b.Property("ElPresentFlag") - .HasColumnType("integer"); - - b.Property("Height") - .HasColumnType("integer"); - - b.Property("IsAnamorphic") - .HasColumnType("boolean"); - - b.Property("IsAvc") - .HasColumnType("boolean"); - - b.Property("IsDefault") - .HasColumnType("boolean"); - - b.Property("IsExternal") - .HasColumnType("boolean"); - - b.Property("IsForced") - .HasColumnType("boolean"); - - b.Property("IsHearingImpaired") - .HasColumnType("boolean"); - - b.Property("IsInterlaced") - .HasColumnType("boolean"); - - b.Property("KeyFrames") - .HasColumnType("text"); - - b.Property("Language") - .HasColumnType("text"); - - b.Property("Level") - .HasColumnType("real"); - - b.Property("NalLengthSize") - .HasColumnType("text"); - - b.Property("Path") - .HasColumnType("text"); - - b.Property("PixelFormat") - .HasColumnType("text"); - - b.Property("Profile") - .HasColumnType("text"); - - b.Property("RealFrameRate") - .HasColumnType("real"); - - b.Property("RefFrames") - .HasColumnType("integer"); - - b.Property("Rotation") - .HasColumnType("integer"); - - b.Property("RpuPresentFlag") - .HasColumnType("integer"); - - b.Property("SampleRate") - .HasColumnType("integer"); - - b.Property("StreamType") - .HasColumnType("integer"); - - b.Property("TimeBase") - .HasColumnType("text"); - - b.Property("Title") - .HasColumnType("text"); - - b.Property("Width") - .HasColumnType("integer"); - - b.HasKey("ItemId", "StreamIndex"); - - b.HasIndex("StreamIndex"); - - b.HasIndex("StreamType"); - - b.HasIndex("StreamIndex", "StreamType"); - - b.HasIndex("StreamIndex", "StreamType", "Language"); - - b.ToTable("MediaStreamInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.People", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("PersonType") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.ToTable("Peoples"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("PeopleId") - .HasColumnType("uuid"); - - b.Property("ListOrder") - .HasColumnType("integer"); - - b.Property("Role") - .HasColumnType("text"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("ItemId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.HasIndex("ItemId", "ListOrder"); - - b.HasIndex("ItemId", "SortOrder"); - - b.ToTable("PeopleBaseItemMap"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Kind") - .HasColumnType("integer"); - - b.Property("Permission_Permissions_Guid") - .HasColumnType("uuid"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("bigint"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("Value") - .HasColumnType("boolean"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Permissions"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Kind") - .HasColumnType("integer"); - - b.Property("Preference_Preferences_Guid") - .HasColumnType("uuid"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("bigint"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(65535) - .HasColumnType("character varying(65535)"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Preferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("text"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastActivity") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.HasKey("Id"); - - b.HasIndex("AccessToken") - .IsUnique(); - - b.ToTable("ApiKeys"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("text"); - - b.Property("AppName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("AppVersion") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastActivity") - .HasColumnType("timestamp with time zone"); - - b.Property("DateModified") - .HasColumnType("timestamp with time zone"); - - b.Property("DeviceId") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("DeviceName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId"); - - b.HasIndex("AccessToken", "DateLastActivity"); - - b.HasIndex("DeviceId", "DateLastActivity"); - - b.HasIndex("UserId", "DeviceId"); - - b.ToTable("Devices"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CustomName") - .HasColumnType("text"); - - b.Property("DeviceId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId") - .IsUnique(); - - b.ToTable("DeviceOptions"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("Width") - .HasColumnType("integer"); - - b.Property("Bandwidth") - .HasColumnType("integer"); - - b.Property("Height") - .HasColumnType("integer"); - - b.Property("Interval") - .HasColumnType("integer"); - - b.Property("ThumbnailCount") - .HasColumnType("integer"); - - b.Property("TileHeight") - .HasColumnType("integer"); - - b.Property("TileWidth") - .HasColumnType("integer"); - - b.HasKey("ItemId", "Width"); - - b.ToTable("TrickplayInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AudioLanguagePreference") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AuthenticationProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CastReceiverId") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DisplayCollectionsView") - .HasColumnType("boolean"); - - b.Property("DisplayMissingEpisodes") - .HasColumnType("boolean"); - - b.Property("EnableAutoLogin") - .HasColumnType("boolean"); - - b.Property("EnableLocalPassword") - .HasColumnType("boolean"); - - b.Property("EnableNextEpisodeAutoPlay") - .HasColumnType("boolean"); - - b.Property("EnableUserPreferenceAccess") - .HasColumnType("boolean"); - - b.Property("HidePlayedInLatest") - .HasColumnType("boolean"); - - b.Property("InternalId") - .HasColumnType("bigint"); - - b.Property("InvalidLoginAttemptCount") - .HasColumnType("integer"); - - b.Property("LastActivityDate") - .HasColumnType("timestamp with time zone"); - - b.Property("LastLoginDate") - .HasColumnType("timestamp with time zone"); - - b.Property("LoginAttemptsBeforeLockout") - .HasColumnType("integer"); - - b.Property("MaxActiveSessions") - .HasColumnType("integer"); - - b.Property("MaxParentalAgeRating") - .HasColumnType("integer"); - - b.Property("MustUpdatePassword") - .HasColumnType("boolean"); - - b.Property("Password") - .HasMaxLength(65535) - .HasColumnType("character varying(65535)"); - - b.Property("PasswordResetProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("PlayDefaultAudioTrack") - .HasColumnType("boolean"); - - b.Property("RememberAudioSelections") - .HasColumnType("boolean"); - - b.Property("RememberSubtitleSelections") - .HasColumnType("boolean"); - - b.Property("RemoteClientBitrateLimit") - .HasColumnType("integer"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("bigint"); - - b.Property("SubtitleLanguagePreference") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SubtitleMode") - .HasColumnType("integer"); - - b.Property("SyncPlayAccess") - .HasColumnType("integer"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)") - .UseCollation("NOCASE"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("CustomDataKey") - .HasColumnType("text"); - - b.Property("AudioStreamIndex") - .HasColumnType("integer"); - - b.Property("IsFavorite") - .HasColumnType("boolean"); - - b.Property("LastPlayedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Likes") - .HasColumnType("boolean"); - - b.Property("PlayCount") - .HasColumnType("integer"); - - b.Property("PlaybackPositionTicks") - .HasColumnType("bigint"); - - b.Property("Played") - .HasColumnType("boolean"); - - b.Property("Rating") - .HasColumnType("double precision"); - - b.Property("SubtitleStreamIndex") - .HasColumnType("integer"); - - b.HasKey("ItemId", "UserId", "CustomDataKey"); - - b.HasIndex("UserId"); - - b.HasIndex("ItemId", "UserId", "IsFavorite"); - - b.HasIndex("ItemId", "UserId", "LastPlayedDate"); - - b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); - - b.HasIndex("ItemId", "UserId", "Played"); - - b.ToTable("UserData"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("AccessSchedules") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Children") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem") - .WithMany("ParentAncestors") - .HasForeignKey("ParentItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("ParentItem"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany() - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Images") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("LockedFields") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Provider") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("TrailerTypes") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Chapters") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("DisplayPreferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => - { - b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) - .WithMany("HomeSections") - .HasForeignKey("DisplayPreferencesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithOne("ProfileImage") - .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("ItemDisplayPreferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("ItemValues") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue") - .WithMany("BaseItemsMap") - .HasForeignKey("ItemValueId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("ItemValue"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("MediaStreams") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Peoples") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Data.Entities.People", "People") - .WithMany("BaseItems") - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("People"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Permissions") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Preferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => - { - b.HasOne("Jellyfin.Data.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("UserData") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Data.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => - { - b.Navigation("Chapters"); - - b.Navigation("Children"); - - b.Navigation("Images"); - - b.Navigation("ItemValues"); - - b.Navigation("LockedFields"); - - b.Navigation("MediaStreams"); - - b.Navigation("ParentAncestors"); - - b.Navigation("Peoples"); - - b.Navigation("Provider"); - - b.Navigation("TrailerTypes"); - - b.Navigation("UserData"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.Navigation("HomeSections"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => - { - b.Navigation("BaseItemsMap"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.People", b => - { - b.Navigation("BaseItems"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.User", b => - { - b.Navigation("AccessSchedules"); - - b.Navigation("DisplayPreferences"); - - b.Navigation("ItemDisplayPreferences"); - - b.Navigation("Permissions"); - - b.Navigation("Preferences"); - - b.Navigation("ProfileImage"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs deleted file mode 100644 index ad1d28b136..0000000000 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250127174201_InitMigration.cs +++ /dev/null @@ -1,1106 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Jellyfin.Database.Providers.PgSql.Migrations -{ - /// - public partial class InitMigration : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "ActivityLogs", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), - Overview = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - ShortOverview = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - Type = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - ItemId = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - DateCreated = table.Column(type: "timestamp with time zone", nullable: false), - LogSeverity = table.Column(type: "integer", nullable: false), - RowVersion = table.Column(type: "bigint", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ActivityLogs", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ApiKeys", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - DateCreated = table.Column(type: "timestamp with time zone", nullable: false), - DateLastActivity = table.Column(type: "timestamp with time zone", nullable: false), - Name = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - AccessToken = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ApiKeys", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "BaseItems", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Type = table.Column(type: "text", nullable: false), - Data = table.Column(type: "text", nullable: true), - Path = table.Column(type: "text", nullable: true), - StartDate = table.Column(type: "timestamp with time zone", nullable: false), - EndDate = table.Column(type: "timestamp with time zone", nullable: false), - ChannelId = table.Column(type: "text", nullable: true), - IsMovie = table.Column(type: "boolean", nullable: false), - CommunityRating = table.Column(type: "real", nullable: true), - CustomRating = table.Column(type: "text", nullable: true), - IndexNumber = table.Column(type: "integer", nullable: true), - IsLocked = table.Column(type: "boolean", nullable: false), - Name = table.Column(type: "text", nullable: true), - OfficialRating = table.Column(type: "text", nullable: true), - MediaType = table.Column(type: "text", nullable: true), - Overview = table.Column(type: "text", nullable: true), - ParentIndexNumber = table.Column(type: "integer", nullable: true), - PremiereDate = table.Column(type: "timestamp with time zone", nullable: true), - ProductionYear = table.Column(type: "integer", nullable: true), - Genres = table.Column(type: "text", nullable: true), - SortName = table.Column(type: "text", nullable: true), - ForcedSortName = table.Column(type: "text", nullable: true), - RunTimeTicks = table.Column(type: "bigint", nullable: true), - DateCreated = table.Column(type: "timestamp with time zone", nullable: true), - DateModified = table.Column(type: "timestamp with time zone", nullable: true), - IsSeries = table.Column(type: "boolean", nullable: false), - EpisodeTitle = table.Column(type: "text", nullable: true), - IsRepeat = table.Column(type: "boolean", nullable: false), - PreferredMetadataLanguage = table.Column(type: "text", nullable: true), - PreferredMetadataCountryCode = table.Column(type: "text", nullable: true), - DateLastRefreshed = table.Column(type: "timestamp with time zone", nullable: true), - DateLastSaved = table.Column(type: "timestamp with time zone", nullable: true), - IsInMixedFolder = table.Column(type: "boolean", nullable: false), - Studios = table.Column(type: "text", nullable: true), - ExternalServiceId = table.Column(type: "text", nullable: true), - Tags = table.Column(type: "text", nullable: true), - IsFolder = table.Column(type: "boolean", nullable: false), - InheritedParentalRatingValue = table.Column(type: "integer", nullable: true), - UnratedType = table.Column(type: "text", nullable: true), - CriticRating = table.Column(type: "real", nullable: true), - CleanName = table.Column(type: "text", nullable: true), - PresentationUniqueKey = table.Column(type: "text", nullable: true), - OriginalTitle = table.Column(type: "text", nullable: true), - PrimaryVersionId = table.Column(type: "text", nullable: true), - DateLastMediaAdded = table.Column(type: "timestamp with time zone", nullable: true), - Album = table.Column(type: "text", nullable: true), - LUFS = table.Column(type: "real", nullable: true), - NormalizationGain = table.Column(type: "real", nullable: true), - IsVirtualItem = table.Column(type: "boolean", nullable: false), - SeriesName = table.Column(type: "text", nullable: true), - SeasonName = table.Column(type: "text", nullable: true), - ExternalSeriesId = table.Column(type: "text", nullable: true), - Tagline = table.Column(type: "text", nullable: true), - ProductionLocations = table.Column(type: "text", nullable: true), - ExtraIds = table.Column(type: "text", nullable: true), - TotalBitrate = table.Column(type: "integer", nullable: true), - ExtraType = table.Column(type: "integer", nullable: true), - Artists = table.Column(type: "text", nullable: true), - AlbumArtists = table.Column(type: "text", nullable: true), - ExternalId = table.Column(type: "text", nullable: true), - SeriesPresentationUniqueKey = table.Column(type: "text", nullable: true), - ShowId = table.Column(type: "text", nullable: true), - OwnerId = table.Column(type: "text", nullable: true), - Width = table.Column(type: "integer", nullable: true), - Height = table.Column(type: "integer", nullable: true), - Size = table.Column(type: "bigint", nullable: true), - Audio = table.Column(type: "integer", nullable: true), - ParentId = table.Column(type: "uuid", nullable: true), - TopParentId = table.Column(type: "uuid", nullable: true), - SeasonId = table.Column(type: "uuid", nullable: true), - SeriesId = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_BaseItems", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "CustomItemDisplayPreferences", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - UserId = table.Column(type: "uuid", nullable: false), - ItemId = table.Column(type: "uuid", nullable: false), - Client = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - Key = table.Column(type: "text", nullable: false), - Value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_CustomItemDisplayPreferences", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "DeviceOptions", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - DeviceId = table.Column(type: "text", nullable: false), - CustomName = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_DeviceOptions", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "ItemValues", - columns: table => new - { - ItemValueId = table.Column(type: "uuid", nullable: false), - Type = table.Column(type: "integer", nullable: false), - Value = table.Column(type: "text", nullable: false), - CleanValue = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ItemValues", x => x.ItemValueId); - }); - - migrationBuilder.CreateTable( - name: "MediaSegments", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ItemId = table.Column(type: "uuid", nullable: false), - Type = table.Column(type: "integer", nullable: false), - EndTicks = table.Column(type: "bigint", nullable: false), - StartTicks = table.Column(type: "bigint", nullable: false), - SegmentProviderId = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_MediaSegments", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Peoples", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "text", nullable: false), - PersonType = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Peoples", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "TrickplayInfos", - columns: table => new - { - ItemId = table.Column(type: "uuid", nullable: false), - Width = table.Column(type: "integer", nullable: false), - Height = table.Column(type: "integer", nullable: false), - TileWidth = table.Column(type: "integer", nullable: false), - TileHeight = table.Column(type: "integer", nullable: false), - ThumbnailCount = table.Column(type: "integer", nullable: false), - Interval = table.Column(type: "integer", nullable: false), - Bandwidth = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_TrickplayInfos", x => new { x.ItemId, x.Width }); - }); - - migrationBuilder.CreateTable( - name: "Users", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Username = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - Password = table.Column(type: "character varying(65535)", maxLength: 65535, nullable: true), - MustUpdatePassword = table.Column(type: "boolean", nullable: false), - AudioLanguagePreference = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), - AuthenticationProviderId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - PasswordResetProviderId = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - InvalidLoginAttemptCount = table.Column(type: "integer", nullable: false), - LastActivityDate = table.Column(type: "timestamp with time zone", nullable: true), - LastLoginDate = table.Column(type: "timestamp with time zone", nullable: true), - LoginAttemptsBeforeLockout = table.Column(type: "integer", nullable: true), - MaxActiveSessions = table.Column(type: "integer", nullable: false), - SubtitleMode = table.Column(type: "integer", nullable: false), - PlayDefaultAudioTrack = table.Column(type: "boolean", nullable: false), - SubtitleLanguagePreference = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), - DisplayMissingEpisodes = table.Column(type: "boolean", nullable: false), - DisplayCollectionsView = table.Column(type: "boolean", nullable: false), - EnableLocalPassword = table.Column(type: "boolean", nullable: false), - HidePlayedInLatest = table.Column(type: "boolean", nullable: false), - RememberAudioSelections = table.Column(type: "boolean", nullable: false), - RememberSubtitleSelections = table.Column(type: "boolean", nullable: false), - EnableNextEpisodeAutoPlay = table.Column(type: "boolean", nullable: false), - EnableAutoLogin = table.Column(type: "boolean", nullable: false), - EnableUserPreferenceAccess = table.Column(type: "boolean", nullable: false), - MaxParentalAgeRating = table.Column(type: "integer", nullable: true), - RemoteClientBitrateLimit = table.Column(type: "integer", nullable: true), - InternalId = table.Column(type: "bigint", nullable: false), - SyncPlayAccess = table.Column(type: "integer", nullable: false), - CastReceiverId = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - RowVersion = table.Column(type: "bigint", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Users", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "AncestorIds", - columns: table => new - { - ParentItemId = table.Column(type: "uuid", nullable: false), - ItemId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AncestorIds", x => new { x.ItemId, x.ParentItemId }); - table.ForeignKey( - name: "FK_AncestorIds_BaseItems_ItemId", - column: x => x.ItemId, - principalTable: "BaseItems", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_AncestorIds_BaseItems_ParentItemId", - column: x => x.ParentItemId, - principalTable: "BaseItems", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AttachmentStreamInfos", - columns: table => new - { - ItemId = table.Column(type: "uuid", nullable: false), - Index = table.Column(type: "integer", nullable: false), - Codec = table.Column(type: "text", nullable: false), - CodecTag = table.Column(type: "text", nullable: true), - Comment = table.Column(type: "text", nullable: true), - Filename = table.Column(type: "text", nullable: true), - MimeType = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AttachmentStreamInfos", x => new { x.ItemId, x.Index }); - table.ForeignKey( - name: "FK_AttachmentStreamInfos_BaseItems_ItemId", - column: x => x.ItemId, - principalTable: "BaseItems", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "BaseItemImageInfos", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Path = table.Column(type: "text", nullable: false), - DateModified = table.Column(type: "timestamp with time zone", nullable: false), - ImageType = table.Column(type: "integer", nullable: false), - Width = table.Column(type: "integer", nullable: false), - Height = table.Column(type: "integer", nullable: false), - Blurhash = table.Column(type: "bytea", nullable: true), - ItemId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_BaseItemImageInfos", x => x.Id); - table.ForeignKey( - name: "FK_BaseItemImageInfos_BaseItems_ItemId", - column: x => x.ItemId, - principalTable: "BaseItems", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "BaseItemMetadataFields", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false), - ItemId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_BaseItemMetadataFields", x => new { x.Id, x.ItemId }); - table.ForeignKey( - name: "FK_BaseItemMetadataFields_BaseItems_ItemId", - column: x => x.ItemId, - principalTable: "BaseItems", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "BaseItemProviders", - columns: table => new - { - ItemId = table.Column(type: "uuid", nullable: false), - ProviderId = table.Column(type: "text", nullable: false), - ProviderValue = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_BaseItemProviders", x => new { x.ItemId, x.ProviderId }); - table.ForeignKey( - name: "FK_BaseItemProviders_BaseItems_ItemId", - column: x => x.ItemId, - principalTable: "BaseItems", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "BaseItemTrailerTypes", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false), - ItemId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_BaseItemTrailerTypes", x => new { x.Id, x.ItemId }); - table.ForeignKey( - name: "FK_BaseItemTrailerTypes_BaseItems_ItemId", - column: x => x.ItemId, - principalTable: "BaseItems", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Chapters", - columns: table => new - { - ItemId = table.Column(type: "uuid", nullable: false), - ChapterIndex = table.Column(type: "integer", nullable: false), - StartPositionTicks = table.Column(type: "bigint", nullable: false), - Name = table.Column(type: "text", nullable: true), - ImagePath = table.Column(type: "text", nullable: true), - ImageDateModified = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Chapters", x => new { x.ItemId, x.ChapterIndex }); - table.ForeignKey( - name: "FK_Chapters_BaseItems_ItemId", - column: x => x.ItemId, - principalTable: "BaseItems", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "MediaStreamInfos", - columns: table => new - { - ItemId = table.Column(type: "uuid", nullable: false), - StreamIndex = table.Column(type: "integer", nullable: false), - StreamType = table.Column(type: "integer", nullable: false), - Codec = table.Column(type: "text", nullable: true), - Language = table.Column(type: "text", nullable: true), - ChannelLayout = table.Column(type: "text", nullable: true), - Profile = table.Column(type: "text", nullable: true), - AspectRatio = table.Column(type: "text", nullable: true), - Path = table.Column(type: "text", nullable: true), - IsInterlaced = table.Column(type: "boolean", nullable: true), - BitRate = table.Column(type: "integer", nullable: true), - Channels = table.Column(type: "integer", nullable: true), - SampleRate = table.Column(type: "integer", nullable: true), - IsDefault = table.Column(type: "boolean", nullable: false), - IsForced = table.Column(type: "boolean", nullable: false), - IsExternal = table.Column(type: "boolean", nullable: false), - Height = table.Column(type: "integer", nullable: true), - Width = table.Column(type: "integer", nullable: true), - AverageFrameRate = table.Column(type: "real", nullable: true), - RealFrameRate = table.Column(type: "real", nullable: true), - Level = table.Column(type: "real", nullable: true), - PixelFormat = table.Column(type: "text", nullable: true), - BitDepth = table.Column(type: "integer", nullable: true), - IsAnamorphic = table.Column(type: "boolean", nullable: true), - RefFrames = table.Column(type: "integer", nullable: true), - CodecTag = table.Column(type: "text", nullable: true), - Comment = table.Column(type: "text", nullable: true), - NalLengthSize = table.Column(type: "text", nullable: true), - IsAvc = table.Column(type: "boolean", nullable: true), - Title = table.Column(type: "text", nullable: true), - TimeBase = table.Column(type: "text", nullable: true), - CodecTimeBase = table.Column(type: "text", nullable: true), - ColorPrimaries = table.Column(type: "text", nullable: true), - ColorSpace = table.Column(type: "text", nullable: true), - ColorTransfer = table.Column(type: "text", nullable: true), - DvVersionMajor = table.Column(type: "integer", nullable: true), - DvVersionMinor = table.Column(type: "integer", nullable: true), - DvProfile = table.Column(type: "integer", nullable: true), - DvLevel = table.Column(type: "integer", nullable: true), - RpuPresentFlag = table.Column(type: "integer", nullable: true), - ElPresentFlag = table.Column(type: "integer", nullable: true), - BlPresentFlag = table.Column(type: "integer", nullable: true), - DvBlSignalCompatibilityId = table.Column(type: "integer", nullable: true), - IsHearingImpaired = table.Column(type: "boolean", nullable: true), - Rotation = table.Column(type: "integer", nullable: true), - KeyFrames = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_MediaStreamInfos", x => new { x.ItemId, x.StreamIndex }); - table.ForeignKey( - name: "FK_MediaStreamInfos_BaseItems_ItemId", - column: x => x.ItemId, - principalTable: "BaseItems", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ItemValuesMap", - columns: table => new - { - ItemId = table.Column(type: "uuid", nullable: false), - ItemValueId = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ItemValuesMap", x => new { x.ItemValueId, x.ItemId }); - table.ForeignKey( - name: "FK_ItemValuesMap_BaseItems_ItemId", - column: x => x.ItemId, - principalTable: "BaseItems", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ItemValuesMap_ItemValues_ItemValueId", - column: x => x.ItemValueId, - principalTable: "ItemValues", - principalColumn: "ItemValueId", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "PeopleBaseItemMap", - columns: table => new - { - ItemId = table.Column(type: "uuid", nullable: false), - PeopleId = table.Column(type: "uuid", nullable: false), - SortOrder = table.Column(type: "integer", nullable: true), - ListOrder = table.Column(type: "integer", nullable: true), - Role = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_PeopleBaseItemMap", x => new { x.ItemId, x.PeopleId }); - table.ForeignKey( - name: "FK_PeopleBaseItemMap_BaseItems_ItemId", - column: x => x.ItemId, - principalTable: "BaseItems", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_PeopleBaseItemMap_Peoples_PeopleId", - column: x => x.PeopleId, - principalTable: "Peoples", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "AccessSchedules", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - UserId = table.Column(type: "uuid", nullable: false), - DayOfWeek = table.Column(type: "integer", nullable: false), - StartHour = table.Column(type: "double precision", nullable: false), - EndHour = table.Column(type: "double precision", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AccessSchedules", x => x.Id); - table.ForeignKey( - name: "FK_AccessSchedules_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Devices", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - UserId = table.Column(type: "uuid", nullable: false), - AccessToken = table.Column(type: "text", nullable: false), - AppName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - AppVersion = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - DeviceName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - DeviceId = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - IsActive = table.Column(type: "boolean", nullable: false), - DateCreated = table.Column(type: "timestamp with time zone", nullable: false), - DateModified = table.Column(type: "timestamp with time zone", nullable: false), - DateLastActivity = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Devices", x => x.Id); - table.ForeignKey( - name: "FK_Devices_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "DisplayPreferences", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - UserId = table.Column(type: "uuid", nullable: false), - ItemId = table.Column(type: "uuid", nullable: false), - Client = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - ShowSidebar = table.Column(type: "boolean", nullable: false), - ShowBackdrop = table.Column(type: "boolean", nullable: false), - ScrollDirection = table.Column(type: "integer", nullable: false), - IndexBy = table.Column(type: "integer", nullable: true), - SkipForwardLength = table.Column(type: "integer", nullable: false), - SkipBackwardLength = table.Column(type: "integer", nullable: false), - ChromecastVersion = table.Column(type: "integer", nullable: false), - EnableNextVideoInfoOverlay = table.Column(type: "boolean", nullable: false), - DashboardTheme = table.Column(type: "character varying(32)", maxLength: 32, nullable: true), - TvHome = table.Column(type: "character varying(32)", maxLength: 32, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_DisplayPreferences", x => x.Id); - table.ForeignKey( - name: "FK_DisplayPreferences_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ImageInfos", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - UserId = table.Column(type: "uuid", nullable: true), - Path = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), - LastModified = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ImageInfos", x => x.Id); - table.ForeignKey( - name: "FK_ImageInfos_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ItemDisplayPreferences", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - UserId = table.Column(type: "uuid", nullable: false), - ItemId = table.Column(type: "uuid", nullable: false), - Client = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - ViewType = table.Column(type: "integer", nullable: false), - RememberIndexing = table.Column(type: "boolean", nullable: false), - IndexBy = table.Column(type: "integer", nullable: true), - RememberSorting = table.Column(type: "boolean", nullable: false), - SortBy = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - SortOrder = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ItemDisplayPreferences", x => x.Id); - table.ForeignKey( - name: "FK_ItemDisplayPreferences_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Permissions", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - UserId = table.Column(type: "uuid", nullable: true), - Kind = table.Column(type: "integer", nullable: false), - Value = table.Column(type: "boolean", nullable: false), - RowVersion = table.Column(type: "bigint", nullable: false), - Permission_Permissions_Guid = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Permissions", x => x.Id); - table.ForeignKey( - name: "FK_Permissions_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Preferences", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - UserId = table.Column(type: "uuid", nullable: true), - Kind = table.Column(type: "integer", nullable: false), - Value = table.Column(type: "character varying(65535)", maxLength: 65535, nullable: false), - RowVersion = table.Column(type: "bigint", nullable: false), - Preference_Preferences_Guid = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Preferences", x => x.Id); - table.ForeignKey( - name: "FK_Preferences_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "UserData", - columns: table => new - { - CustomDataKey = table.Column(type: "text", nullable: false), - ItemId = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - Rating = table.Column(type: "double precision", nullable: true), - PlaybackPositionTicks = table.Column(type: "bigint", nullable: false), - PlayCount = table.Column(type: "integer", nullable: false), - IsFavorite = table.Column(type: "boolean", nullable: false), - LastPlayedDate = table.Column(type: "timestamp with time zone", nullable: true), - Played = table.Column(type: "boolean", nullable: false), - AudioStreamIndex = table.Column(type: "integer", nullable: true), - SubtitleStreamIndex = table.Column(type: "integer", nullable: true), - Likes = table.Column(type: "boolean", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_UserData", x => new { x.ItemId, x.UserId, x.CustomDataKey }); - table.ForeignKey( - name: "FK_UserData_BaseItems_ItemId", - column: x => x.ItemId, - principalTable: "BaseItems", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_UserData_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "HomeSection", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - DisplayPreferencesId = table.Column(type: "integer", nullable: false), - Order = table.Column(type: "integer", nullable: false), - Type = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_HomeSection", x => x.Id); - table.ForeignKey( - name: "FK_HomeSection_DisplayPreferences_DisplayPreferencesId", - column: x => x.DisplayPreferencesId, - principalTable: "DisplayPreferences", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_AccessSchedules_UserId", - table: "AccessSchedules", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_ActivityLogs_DateCreated", - table: "ActivityLogs", - column: "DateCreated"); - - migrationBuilder.CreateIndex( - name: "IX_AncestorIds_ParentItemId", - table: "AncestorIds", - column: "ParentItemId"); - - migrationBuilder.CreateIndex( - name: "IX_ApiKeys_AccessToken", - table: "ApiKeys", - column: "AccessToken", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_BaseItemImageInfos_ItemId", - table: "BaseItemImageInfos", - column: "ItemId"); - - migrationBuilder.CreateIndex( - name: "IX_BaseItemMetadataFields_ItemId", - table: "BaseItemMetadataFields", - column: "ItemId"); - - migrationBuilder.CreateIndex( - name: "IX_BaseItemProviders_ProviderId_ProviderValue_ItemId", - table: "BaseItemProviders", - columns: new[] { "ProviderId", "ProviderValue", "ItemId" }); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_Id_Type_IsFolder_IsVirtualItem", - table: "BaseItems", - columns: new[] { "Id", "Type", "IsFolder", "IsVirtualItem" }); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_IsFolder_TopParentId_IsVirtualItem_PresentationUn~", - table: "BaseItems", - columns: new[] { "IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated" }); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_MediaType_TopParentId_IsVirtualItem_PresentationU~", - table: "BaseItems", - columns: new[] { "MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey" }); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_ParentId", - table: "BaseItems", - column: "ParentId"); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_Path", - table: "BaseItems", - column: "Path"); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_PresentationUniqueKey", - table: "BaseItems", - column: "PresentationUniqueKey"); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_TopParentId_Id", - table: "BaseItems", - columns: new[] { "TopParentId", "Id" }); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_IsFolder_IsVirtu~", - table: "BaseItems", - columns: new[] { "Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem" }); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_Type_SeriesPresentationUniqueKey_PresentationUniq~", - table: "BaseItems", - columns: new[] { "Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName" }); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_Type_TopParentId_Id", - table: "BaseItems", - columns: new[] { "Type", "TopParentId", "Id" }); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_Type_TopParentId_IsVirtualItem_PresentationUnique~", - table: "BaseItems", - columns: new[] { "Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated" }); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_Type_TopParentId_PresentationUniqueKey", - table: "BaseItems", - columns: new[] { "Type", "TopParentId", "PresentationUniqueKey" }); - - migrationBuilder.CreateIndex( - name: "IX_BaseItems_Type_TopParentId_StartDate", - table: "BaseItems", - columns: new[] { "Type", "TopParentId", "StartDate" }); - - migrationBuilder.CreateIndex( - name: "IX_BaseItemTrailerTypes_ItemId", - table: "BaseItemTrailerTypes", - column: "ItemId"); - - migrationBuilder.CreateIndex( - name: "IX_CustomItemDisplayPreferences_UserId_ItemId_Client_Key", - table: "CustomItemDisplayPreferences", - columns: new[] { "UserId", "ItemId", "Client", "Key" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_DeviceOptions_DeviceId", - table: "DeviceOptions", - column: "DeviceId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Devices_AccessToken_DateLastActivity", - table: "Devices", - columns: new[] { "AccessToken", "DateLastActivity" }); - - migrationBuilder.CreateIndex( - name: "IX_Devices_DeviceId", - table: "Devices", - column: "DeviceId"); - - migrationBuilder.CreateIndex( - name: "IX_Devices_DeviceId_DateLastActivity", - table: "Devices", - columns: new[] { "DeviceId", "DateLastActivity" }); - - migrationBuilder.CreateIndex( - name: "IX_Devices_UserId_DeviceId", - table: "Devices", - columns: new[] { "UserId", "DeviceId" }); - - migrationBuilder.CreateIndex( - name: "IX_DisplayPreferences_UserId_ItemId_Client", - table: "DisplayPreferences", - columns: new[] { "UserId", "ItemId", "Client" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_HomeSection_DisplayPreferencesId", - table: "HomeSection", - column: "DisplayPreferencesId"); - - migrationBuilder.CreateIndex( - name: "IX_ImageInfos_UserId", - table: "ImageInfos", - column: "UserId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ItemDisplayPreferences_UserId", - table: "ItemDisplayPreferences", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_ItemValues_Type_CleanValue", - table: "ItemValues", - columns: new[] { "Type", "CleanValue" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ItemValuesMap_ItemId", - table: "ItemValuesMap", - column: "ItemId"); - - migrationBuilder.CreateIndex( - name: "IX_MediaStreamInfos_StreamIndex", - table: "MediaStreamInfos", - column: "StreamIndex"); - - migrationBuilder.CreateIndex( - name: "IX_MediaStreamInfos_StreamIndex_StreamType", - table: "MediaStreamInfos", - columns: new[] { "StreamIndex", "StreamType" }); - - migrationBuilder.CreateIndex( - name: "IX_MediaStreamInfos_StreamIndex_StreamType_Language", - table: "MediaStreamInfos", - columns: new[] { "StreamIndex", "StreamType", "Language" }); - - migrationBuilder.CreateIndex( - name: "IX_MediaStreamInfos_StreamType", - table: "MediaStreamInfos", - column: "StreamType"); - - migrationBuilder.CreateIndex( - name: "IX_PeopleBaseItemMap_ItemId_ListOrder", - table: "PeopleBaseItemMap", - columns: new[] { "ItemId", "ListOrder" }); - - migrationBuilder.CreateIndex( - name: "IX_PeopleBaseItemMap_ItemId_SortOrder", - table: "PeopleBaseItemMap", - columns: new[] { "ItemId", "SortOrder" }); - - migrationBuilder.CreateIndex( - name: "IX_PeopleBaseItemMap_PeopleId", - table: "PeopleBaseItemMap", - column: "PeopleId"); - - migrationBuilder.CreateIndex( - name: "IX_Peoples_Name", - table: "Peoples", - column: "Name"); - - // this was edited manually because "UserId" is a reserved name in pgsql - migrationBuilder.CreateIndex( - name: "IX_Permissions_UserId_Kind", - table: "Permissions", - columns: new[] { "UserId", "Kind" }, - unique: true, - filter: "\"Permissions\".\"UserId\" IS NOT NULL"); - - // this was edited manually because "UserId" is a reserved name in pgsql - migrationBuilder.CreateIndex( - name: "IX_Preferences_UserId_Kind", - table: "Preferences", - columns: new[] { "UserId", "Kind" }, - unique: true, - filter: "\"Preferences\".\"UserId\" IS NOT NULL"); - - migrationBuilder.CreateIndex( - name: "IX_UserData_ItemId_UserId_IsFavorite", - table: "UserData", - columns: new[] { "ItemId", "UserId", "IsFavorite" }); - - migrationBuilder.CreateIndex( - name: "IX_UserData_ItemId_UserId_LastPlayedDate", - table: "UserData", - columns: new[] { "ItemId", "UserId", "LastPlayedDate" }); - - migrationBuilder.CreateIndex( - name: "IX_UserData_ItemId_UserId_PlaybackPositionTicks", - table: "UserData", - columns: new[] { "ItemId", "UserId", "PlaybackPositionTicks" }); - - migrationBuilder.CreateIndex( - name: "IX_UserData_ItemId_UserId_Played", - table: "UserData", - columns: new[] { "ItemId", "UserId", "Played" }); - - migrationBuilder.CreateIndex( - name: "IX_UserData_UserId", - table: "UserData", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_Users_Username", - table: "Users", - column: "Username", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AccessSchedules"); - - migrationBuilder.DropTable( - name: "ActivityLogs"); - - migrationBuilder.DropTable( - name: "AncestorIds"); - - migrationBuilder.DropTable( - name: "ApiKeys"); - - migrationBuilder.DropTable( - name: "AttachmentStreamInfos"); - - migrationBuilder.DropTable( - name: "BaseItemImageInfos"); - - migrationBuilder.DropTable( - name: "BaseItemMetadataFields"); - - migrationBuilder.DropTable( - name: "BaseItemProviders"); - - migrationBuilder.DropTable( - name: "BaseItemTrailerTypes"); - - migrationBuilder.DropTable( - name: "Chapters"); - - migrationBuilder.DropTable( - name: "CustomItemDisplayPreferences"); - - migrationBuilder.DropTable( - name: "DeviceOptions"); - - migrationBuilder.DropTable( - name: "Devices"); - - migrationBuilder.DropTable( - name: "HomeSection"); - - migrationBuilder.DropTable( - name: "ImageInfos"); - - migrationBuilder.DropTable( - name: "ItemDisplayPreferences"); - - migrationBuilder.DropTable( - name: "ItemValuesMap"); - - migrationBuilder.DropTable( - name: "MediaSegments"); - - migrationBuilder.DropTable( - name: "MediaStreamInfos"); - - migrationBuilder.DropTable( - name: "PeopleBaseItemMap"); - - migrationBuilder.DropTable( - name: "Permissions"); - - migrationBuilder.DropTable( - name: "Preferences"); - - migrationBuilder.DropTable( - name: "TrickplayInfos"); - - migrationBuilder.DropTable( - name: "UserData"); - - migrationBuilder.DropTable( - name: "DisplayPreferences"); - - migrationBuilder.DropTable( - name: "ItemValues"); - - migrationBuilder.DropTable( - name: "Peoples"); - - migrationBuilder.DropTable( - name: "BaseItems"); - - migrationBuilder.DropTable( - name: "Users"); - } - } -} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.Designer.cs deleted file mode 100644 index 6e8aba54bf..0000000000 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.Designer.cs +++ /dev/null @@ -1,1623 +0,0 @@ -// -using System; -using Jellyfin.Server.Implementations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Jellyfin.Database.Providers.PgSql.Migrations -{ - [DbContext(typeof(JellyfinDbContext))] - [Migration("20250205183152_MakeStartEndDateNullable")] - partial class MakeStartEndDateNullable - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.1") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DayOfWeek") - .HasColumnType("integer"); - - b.Property("EndHour") - .HasColumnType("double precision"); - - b.Property("StartHour") - .HasColumnType("double precision"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AccessSchedules"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("ItemId") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LogSeverity") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Overview") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("bigint"); - - b.Property("ShortOverview") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("DateCreated"); - - b.ToTable("ActivityLogs"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("ParentItemId") - .HasColumnType("uuid"); - - b.HasKey("ItemId", "ParentItemId"); - - b.HasIndex("ParentItemId"); - - b.ToTable("AncestorIds"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("Index") - .HasColumnType("integer"); - - b.Property("Codec") - .IsRequired() - .HasColumnType("text"); - - b.Property("CodecTag") - .HasColumnType("text"); - - b.Property("Comment") - .HasColumnType("text"); - - b.Property("Filename") - .HasColumnType("text"); - - b.Property("MimeType") - .HasColumnType("text"); - - b.HasKey("ItemId", "Index"); - - b.ToTable("AttachmentStreamInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Album") - .HasColumnType("text"); - - b.Property("AlbumArtists") - .HasColumnType("text"); - - b.Property("Artists") - .HasColumnType("text"); - - b.Property("Audio") - .HasColumnType("integer"); - - b.Property("ChannelId") - .HasColumnType("text"); - - b.Property("CleanName") - .HasColumnType("text"); - - b.Property("CommunityRating") - .HasColumnType("real"); - - b.Property("CriticRating") - .HasColumnType("real"); - - b.Property("CustomRating") - .HasColumnType("text"); - - b.Property("Data") - .HasColumnType("text"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastMediaAdded") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastRefreshed") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastSaved") - .HasColumnType("timestamp with time zone"); - - b.Property("DateModified") - .HasColumnType("timestamp with time zone"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("EpisodeTitle") - .HasColumnType("text"); - - b.Property("ExternalId") - .HasColumnType("text"); - - b.Property("ExternalSeriesId") - .HasColumnType("text"); - - b.Property("ExternalServiceId") - .HasColumnType("text"); - - b.Property("ExtraIds") - .HasColumnType("text"); - - b.Property("ExtraType") - .HasColumnType("integer"); - - b.Property("ForcedSortName") - .HasColumnType("text"); - - b.Property("Genres") - .HasColumnType("text"); - - b.Property("Height") - .HasColumnType("integer"); - - b.Property("IndexNumber") - .HasColumnType("integer"); - - b.Property("InheritedParentalRatingValue") - .HasColumnType("integer"); - - b.Property("IsFolder") - .HasColumnType("boolean"); - - b.Property("IsInMixedFolder") - .HasColumnType("boolean"); - - b.Property("IsLocked") - .HasColumnType("boolean"); - - b.Property("IsMovie") - .HasColumnType("boolean"); - - b.Property("IsRepeat") - .HasColumnType("boolean"); - - b.Property("IsSeries") - .HasColumnType("boolean"); - - b.Property("IsVirtualItem") - .HasColumnType("boolean"); - - b.Property("LUFS") - .HasColumnType("real"); - - b.Property("MediaType") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("NormalizationGain") - .HasColumnType("real"); - - b.Property("OfficialRating") - .HasColumnType("text"); - - b.Property("OriginalTitle") - .HasColumnType("text"); - - b.Property("Overview") - .HasColumnType("text"); - - b.Property("OwnerId") - .HasColumnType("text"); - - b.Property("ParentId") - .HasColumnType("uuid"); - - b.Property("ParentIndexNumber") - .HasColumnType("integer"); - - b.Property("Path") - .HasColumnType("text"); - - b.Property("PreferredMetadataCountryCode") - .HasColumnType("text"); - - b.Property("PreferredMetadataLanguage") - .HasColumnType("text"); - - b.Property("PremiereDate") - .HasColumnType("timestamp with time zone"); - - b.Property("PresentationUniqueKey") - .HasColumnType("text"); - - b.Property("PrimaryVersionId") - .HasColumnType("text"); - - b.Property("ProductionLocations") - .HasColumnType("text"); - - b.Property("ProductionYear") - .HasColumnType("integer"); - - b.Property("RunTimeTicks") - .HasColumnType("bigint"); - - b.Property("SeasonId") - .HasColumnType("uuid"); - - b.Property("SeasonName") - .HasColumnType("text"); - - b.Property("SeriesId") - .HasColumnType("uuid"); - - b.Property("SeriesName") - .HasColumnType("text"); - - b.Property("SeriesPresentationUniqueKey") - .HasColumnType("text"); - - b.Property("ShowId") - .HasColumnType("text"); - - b.Property("Size") - .HasColumnType("bigint"); - - b.Property("SortName") - .HasColumnType("text"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Studios") - .HasColumnType("text"); - - b.Property("Tagline") - .HasColumnType("text"); - - b.Property("Tags") - .HasColumnType("text"); - - b.Property("TopParentId") - .HasColumnType("uuid"); - - b.Property("TotalBitrate") - .HasColumnType("integer"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UnratedType") - .HasColumnType("text"); - - b.Property("Width") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ParentId"); - - b.HasIndex("Path"); - - b.HasIndex("PresentationUniqueKey"); - - b.HasIndex("TopParentId", "Id"); - - b.HasIndex("Type", "TopParentId", "Id"); - - b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); - - b.HasIndex("Type", "TopParentId", "StartDate"); - - b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); - - b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); - - b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); - - b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); - - b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); - - b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); - - b.ToTable("BaseItems"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Blurhash") - .HasColumnType("bytea"); - - b.Property("DateModified") - .HasColumnType("timestamp with time zone"); - - b.Property("Height") - .HasColumnType("integer"); - - b.Property("ImageType") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("Path") - .IsRequired() - .HasColumnType("text"); - - b.Property("Width") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ItemId"); - - b.ToTable("BaseItemImageInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => - { - b.Property("Id") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.HasKey("Id", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("BaseItemMetadataFields"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("ProviderId") - .HasColumnType("text"); - - b.Property("ProviderValue") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ItemId", "ProviderId"); - - b.HasIndex("ProviderId", "ProviderValue", "ItemId"); - - b.ToTable("BaseItemProviders"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => - { - b.Property("Id") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.HasKey("Id", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("BaseItemTrailerTypes"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("ChapterIndex") - .HasColumnType("integer"); - - b.Property("ImageDateModified") - .HasColumnType("timestamp with time zone"); - - b.Property("ImagePath") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("StartPositionTicks") - .HasColumnType("bigint"); - - b.HasKey("ItemId", "ChapterIndex"); - - b.ToTable("Chapters"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("Value") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client", "Key") - .IsUnique(); - - b.ToTable("CustomItemDisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChromecastVersion") - .HasColumnType("integer"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DashboardTheme") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("EnableNextVideoInfoOverlay") - .HasColumnType("boolean"); - - b.Property("IndexBy") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("ScrollDirection") - .HasColumnType("integer"); - - b.Property("ShowBackdrop") - .HasColumnType("boolean"); - - b.Property("ShowSidebar") - .HasColumnType("boolean"); - - b.Property("SkipBackwardLength") - .HasColumnType("integer"); - - b.Property("SkipForwardLength") - .HasColumnType("integer"); - - b.Property("TvHome") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client") - .IsUnique(); - - b.ToTable("DisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DisplayPreferencesId") - .HasColumnType("integer"); - - b.Property("Order") - .HasColumnType("integer"); - - b.Property("Type") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("DisplayPreferencesId"); - - b.ToTable("HomeSection"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("LastModified") - .HasColumnType("timestamp with time zone"); - - b.Property("Path") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("ImageInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("IndexBy") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("RememberIndexing") - .HasColumnType("boolean"); - - b.Property("RememberSorting") - .HasColumnType("boolean"); - - b.Property("SortBy") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("ViewType") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("ItemDisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => - { - b.Property("ItemValueId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CleanValue") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .HasColumnType("integer"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ItemValueId"); - - b.HasIndex("Type", "CleanValue") - .IsUnique(); - - b.ToTable("ItemValues"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => - { - b.Property("ItemValueId") - .HasColumnType("uuid"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.HasKey("ItemValueId", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("ItemValuesMap"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EndTicks") - .HasColumnType("bigint"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("SegmentProviderId") - .IsRequired() - .HasColumnType("text"); - - b.Property("StartTicks") - .HasColumnType("bigint"); - - b.Property("Type") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.ToTable("MediaSegments"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("StreamIndex") - .HasColumnType("integer"); - - b.Property("AspectRatio") - .HasColumnType("text"); - - b.Property("AverageFrameRate") - .HasColumnType("real"); - - b.Property("BitDepth") - .HasColumnType("integer"); - - b.Property("BitRate") - .HasColumnType("integer"); - - b.Property("BlPresentFlag") - .HasColumnType("integer"); - - b.Property("ChannelLayout") - .HasColumnType("text"); - - b.Property("Channels") - .HasColumnType("integer"); - - b.Property("Codec") - .HasColumnType("text"); - - b.Property("CodecTag") - .HasColumnType("text"); - - b.Property("CodecTimeBase") - .HasColumnType("text"); - - b.Property("ColorPrimaries") - .HasColumnType("text"); - - b.Property("ColorSpace") - .HasColumnType("text"); - - b.Property("ColorTransfer") - .HasColumnType("text"); - - b.Property("Comment") - .HasColumnType("text"); - - b.Property("DvBlSignalCompatibilityId") - .HasColumnType("integer"); - - b.Property("DvLevel") - .HasColumnType("integer"); - - b.Property("DvProfile") - .HasColumnType("integer"); - - b.Property("DvVersionMajor") - .HasColumnType("integer"); - - b.Property("DvVersionMinor") - .HasColumnType("integer"); - - b.Property("ElPresentFlag") - .HasColumnType("integer"); - - b.Property("Height") - .HasColumnType("integer"); - - b.Property("IsAnamorphic") - .HasColumnType("boolean"); - - b.Property("IsAvc") - .HasColumnType("boolean"); - - b.Property("IsDefault") - .HasColumnType("boolean"); - - b.Property("IsExternal") - .HasColumnType("boolean"); - - b.Property("IsForced") - .HasColumnType("boolean"); - - b.Property("IsHearingImpaired") - .HasColumnType("boolean"); - - b.Property("IsInterlaced") - .HasColumnType("boolean"); - - b.Property("KeyFrames") - .HasColumnType("text"); - - b.Property("Language") - .HasColumnType("text"); - - b.Property("Level") - .HasColumnType("real"); - - b.Property("NalLengthSize") - .HasColumnType("text"); - - b.Property("Path") - .HasColumnType("text"); - - b.Property("PixelFormat") - .HasColumnType("text"); - - b.Property("Profile") - .HasColumnType("text"); - - b.Property("RealFrameRate") - .HasColumnType("real"); - - b.Property("RefFrames") - .HasColumnType("integer"); - - b.Property("Rotation") - .HasColumnType("integer"); - - b.Property("RpuPresentFlag") - .HasColumnType("integer"); - - b.Property("SampleRate") - .HasColumnType("integer"); - - b.Property("StreamType") - .HasColumnType("integer"); - - b.Property("TimeBase") - .HasColumnType("text"); - - b.Property("Title") - .HasColumnType("text"); - - b.Property("Width") - .HasColumnType("integer"); - - b.HasKey("ItemId", "StreamIndex"); - - b.HasIndex("StreamIndex"); - - b.HasIndex("StreamType"); - - b.HasIndex("StreamIndex", "StreamType"); - - b.HasIndex("StreamIndex", "StreamType", "Language"); - - b.ToTable("MediaStreamInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.People", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("PersonType") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.ToTable("Peoples"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("PeopleId") - .HasColumnType("uuid"); - - b.Property("ListOrder") - .HasColumnType("integer"); - - b.Property("Role") - .HasColumnType("text"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("ItemId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.HasIndex("ItemId", "ListOrder"); - - b.HasIndex("ItemId", "SortOrder"); - - b.ToTable("PeopleBaseItemMap"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Kind") - .HasColumnType("integer"); - - b.Property("Permission_Permissions_Guid") - .HasColumnType("uuid"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("bigint"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("Value") - .HasColumnType("boolean"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Permissions"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Kind") - .HasColumnType("integer"); - - b.Property("Preference_Preferences_Guid") - .HasColumnType("uuid"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("bigint"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(65535) - .HasColumnType("character varying(65535)"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Preferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("text"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastActivity") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.HasKey("Id"); - - b.HasIndex("AccessToken") - .IsUnique(); - - b.ToTable("ApiKeys"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("text"); - - b.Property("AppName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("AppVersion") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastActivity") - .HasColumnType("timestamp with time zone"); - - b.Property("DateModified") - .HasColumnType("timestamp with time zone"); - - b.Property("DeviceId") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("DeviceName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId"); - - b.HasIndex("AccessToken", "DateLastActivity"); - - b.HasIndex("DeviceId", "DateLastActivity"); - - b.HasIndex("UserId", "DeviceId"); - - b.ToTable("Devices"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CustomName") - .HasColumnType("text"); - - b.Property("DeviceId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId") - .IsUnique(); - - b.ToTable("DeviceOptions"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("Width") - .HasColumnType("integer"); - - b.Property("Bandwidth") - .HasColumnType("integer"); - - b.Property("Height") - .HasColumnType("integer"); - - b.Property("Interval") - .HasColumnType("integer"); - - b.Property("ThumbnailCount") - .HasColumnType("integer"); - - b.Property("TileHeight") - .HasColumnType("integer"); - - b.Property("TileWidth") - .HasColumnType("integer"); - - b.HasKey("ItemId", "Width"); - - b.ToTable("TrickplayInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AudioLanguagePreference") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AuthenticationProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CastReceiverId") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DisplayCollectionsView") - .HasColumnType("boolean"); - - b.Property("DisplayMissingEpisodes") - .HasColumnType("boolean"); - - b.Property("EnableAutoLogin") - .HasColumnType("boolean"); - - b.Property("EnableLocalPassword") - .HasColumnType("boolean"); - - b.Property("EnableNextEpisodeAutoPlay") - .HasColumnType("boolean"); - - b.Property("EnableUserPreferenceAccess") - .HasColumnType("boolean"); - - b.Property("HidePlayedInLatest") - .HasColumnType("boolean"); - - b.Property("InternalId") - .HasColumnType("bigint"); - - b.Property("InvalidLoginAttemptCount") - .HasColumnType("integer"); - - b.Property("LastActivityDate") - .HasColumnType("timestamp with time zone"); - - b.Property("LastLoginDate") - .HasColumnType("timestamp with time zone"); - - b.Property("LoginAttemptsBeforeLockout") - .HasColumnType("integer"); - - b.Property("MaxActiveSessions") - .HasColumnType("integer"); - - b.Property("MaxParentalAgeRating") - .HasColumnType("integer"); - - b.Property("MustUpdatePassword") - .HasColumnType("boolean"); - - b.Property("Password") - .HasMaxLength(65535) - .HasColumnType("character varying(65535)"); - - b.Property("PasswordResetProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("PlayDefaultAudioTrack") - .HasColumnType("boolean"); - - b.Property("RememberAudioSelections") - .HasColumnType("boolean"); - - b.Property("RememberSubtitleSelections") - .HasColumnType("boolean"); - - b.Property("RemoteClientBitrateLimit") - .HasColumnType("integer"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("bigint"); - - b.Property("SubtitleLanguagePreference") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SubtitleMode") - .HasColumnType("integer"); - - b.Property("SyncPlayAccess") - .HasColumnType("integer"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("CustomDataKey") - .HasColumnType("text"); - - b.Property("AudioStreamIndex") - .HasColumnType("integer"); - - b.Property("IsFavorite") - .HasColumnType("boolean"); - - b.Property("LastPlayedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Likes") - .HasColumnType("boolean"); - - b.Property("PlayCount") - .HasColumnType("integer"); - - b.Property("PlaybackPositionTicks") - .HasColumnType("bigint"); - - b.Property("Played") - .HasColumnType("boolean"); - - b.Property("Rating") - .HasColumnType("double precision"); - - b.Property("SubtitleStreamIndex") - .HasColumnType("integer"); - - b.HasKey("ItemId", "UserId", "CustomDataKey"); - - b.HasIndex("UserId"); - - b.HasIndex("ItemId", "UserId", "IsFavorite"); - - b.HasIndex("ItemId", "UserId", "LastPlayedDate"); - - b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); - - b.HasIndex("ItemId", "UserId", "Played"); - - b.ToTable("UserData"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("AccessSchedules") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Children") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem") - .WithMany("ParentAncestors") - .HasForeignKey("ParentItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("ParentItem"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany() - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Images") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("LockedFields") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Provider") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("TrailerTypes") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Chapters") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("DisplayPreferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => - { - b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) - .WithMany("HomeSections") - .HasForeignKey("DisplayPreferencesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithOne("ProfileImage") - .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("ItemDisplayPreferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("ItemValues") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue") - .WithMany("BaseItemsMap") - .HasForeignKey("ItemValueId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("ItemValue"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("MediaStreams") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Peoples") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Data.Entities.People", "People") - .WithMany("BaseItems") - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("People"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Permissions") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Preferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => - { - b.HasOne("Jellyfin.Data.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("UserData") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Data.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => - { - b.Navigation("Chapters"); - - b.Navigation("Children"); - - b.Navigation("Images"); - - b.Navigation("ItemValues"); - - b.Navigation("LockedFields"); - - b.Navigation("MediaStreams"); - - b.Navigation("ParentAncestors"); - - b.Navigation("Peoples"); - - b.Navigation("Provider"); - - b.Navigation("TrailerTypes"); - - b.Navigation("UserData"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.Navigation("HomeSections"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => - { - b.Navigation("BaseItemsMap"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.People", b => - { - b.Navigation("BaseItems"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.User", b => - { - b.Navigation("AccessSchedules"); - - b.Navigation("DisplayPreferences"); - - b.Navigation("ItemDisplayPreferences"); - - b.Navigation("Permissions"); - - b.Navigation("Preferences"); - - b.Navigation("ProfileImage"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.cs deleted file mode 100644 index 6ed1ddc5aa..0000000000 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Jellyfin.Database.Providers.PgSql.Migrations -{ - /// - public partial class MakeStartEndDateNullable : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "StartDate", - table: "BaseItems", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - - migrationBuilder.AlterColumn( - name: "EndDate", - table: "BaseItems", - type: "timestamp with time zone", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "StartDate", - table: "BaseItems", - type: "timestamp with time zone", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "EndDate", - table: "BaseItems", - type: "timestamp with time zone", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), - oldClrType: typeof(DateTime), - oldType: "timestamp with time zone", - oldNullable: true); - } - } -} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs deleted file mode 100644 index d67fd89d84..0000000000 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs +++ /dev/null @@ -1,1620 +0,0 @@ -// -using System; -using Jellyfin.Server.Implementations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Jellyfin.Database.Providers.PgSql.Migrations -{ - [DbContext(typeof(JellyfinDbContext))] - partial class JellyfinDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.1") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DayOfWeek") - .HasColumnType("integer"); - - b.Property("EndHour") - .HasColumnType("double precision"); - - b.Property("StartHour") - .HasColumnType("double precision"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AccessSchedules"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("ItemId") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("LogSeverity") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Overview") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("bigint"); - - b.Property("ShortOverview") - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("DateCreated"); - - b.ToTable("ActivityLogs"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("ParentItemId") - .HasColumnType("uuid"); - - b.HasKey("ItemId", "ParentItemId"); - - b.HasIndex("ParentItemId"); - - b.ToTable("AncestorIds"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("Index") - .HasColumnType("integer"); - - b.Property("Codec") - .IsRequired() - .HasColumnType("text"); - - b.Property("CodecTag") - .HasColumnType("text"); - - b.Property("Comment") - .HasColumnType("text"); - - b.Property("Filename") - .HasColumnType("text"); - - b.Property("MimeType") - .HasColumnType("text"); - - b.HasKey("ItemId", "Index"); - - b.ToTable("AttachmentStreamInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Album") - .HasColumnType("text"); - - b.Property("AlbumArtists") - .HasColumnType("text"); - - b.Property("Artists") - .HasColumnType("text"); - - b.Property("Audio") - .HasColumnType("integer"); - - b.Property("ChannelId") - .HasColumnType("text"); - - b.Property("CleanName") - .HasColumnType("text"); - - b.Property("CommunityRating") - .HasColumnType("real"); - - b.Property("CriticRating") - .HasColumnType("real"); - - b.Property("CustomRating") - .HasColumnType("text"); - - b.Property("Data") - .HasColumnType("text"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastMediaAdded") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastRefreshed") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastSaved") - .HasColumnType("timestamp with time zone"); - - b.Property("DateModified") - .HasColumnType("timestamp with time zone"); - - b.Property("EndDate") - .HasColumnType("timestamp with time zone"); - - b.Property("EpisodeTitle") - .HasColumnType("text"); - - b.Property("ExternalId") - .HasColumnType("text"); - - b.Property("ExternalSeriesId") - .HasColumnType("text"); - - b.Property("ExternalServiceId") - .HasColumnType("text"); - - b.Property("ExtraIds") - .HasColumnType("text"); - - b.Property("ExtraType") - .HasColumnType("integer"); - - b.Property("ForcedSortName") - .HasColumnType("text"); - - b.Property("Genres") - .HasColumnType("text"); - - b.Property("Height") - .HasColumnType("integer"); - - b.Property("IndexNumber") - .HasColumnType("integer"); - - b.Property("InheritedParentalRatingValue") - .HasColumnType("integer"); - - b.Property("IsFolder") - .HasColumnType("boolean"); - - b.Property("IsInMixedFolder") - .HasColumnType("boolean"); - - b.Property("IsLocked") - .HasColumnType("boolean"); - - b.Property("IsMovie") - .HasColumnType("boolean"); - - b.Property("IsRepeat") - .HasColumnType("boolean"); - - b.Property("IsSeries") - .HasColumnType("boolean"); - - b.Property("IsVirtualItem") - .HasColumnType("boolean"); - - b.Property("LUFS") - .HasColumnType("real"); - - b.Property("MediaType") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("NormalizationGain") - .HasColumnType("real"); - - b.Property("OfficialRating") - .HasColumnType("text"); - - b.Property("OriginalTitle") - .HasColumnType("text"); - - b.Property("Overview") - .HasColumnType("text"); - - b.Property("OwnerId") - .HasColumnType("text"); - - b.Property("ParentId") - .HasColumnType("uuid"); - - b.Property("ParentIndexNumber") - .HasColumnType("integer"); - - b.Property("Path") - .HasColumnType("text"); - - b.Property("PreferredMetadataCountryCode") - .HasColumnType("text"); - - b.Property("PreferredMetadataLanguage") - .HasColumnType("text"); - - b.Property("PremiereDate") - .HasColumnType("timestamp with time zone"); - - b.Property("PresentationUniqueKey") - .HasColumnType("text"); - - b.Property("PrimaryVersionId") - .HasColumnType("text"); - - b.Property("ProductionLocations") - .HasColumnType("text"); - - b.Property("ProductionYear") - .HasColumnType("integer"); - - b.Property("RunTimeTicks") - .HasColumnType("bigint"); - - b.Property("SeasonId") - .HasColumnType("uuid"); - - b.Property("SeasonName") - .HasColumnType("text"); - - b.Property("SeriesId") - .HasColumnType("uuid"); - - b.Property("SeriesName") - .HasColumnType("text"); - - b.Property("SeriesPresentationUniqueKey") - .HasColumnType("text"); - - b.Property("ShowId") - .HasColumnType("text"); - - b.Property("Size") - .HasColumnType("bigint"); - - b.Property("SortName") - .HasColumnType("text"); - - b.Property("StartDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Studios") - .HasColumnType("text"); - - b.Property("Tagline") - .HasColumnType("text"); - - b.Property("Tags") - .HasColumnType("text"); - - b.Property("TopParentId") - .HasColumnType("uuid"); - - b.Property("TotalBitrate") - .HasColumnType("integer"); - - b.Property("Type") - .IsRequired() - .HasColumnType("text"); - - b.Property("UnratedType") - .HasColumnType("text"); - - b.Property("Width") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ParentId"); - - b.HasIndex("Path"); - - b.HasIndex("PresentationUniqueKey"); - - b.HasIndex("TopParentId", "Id"); - - b.HasIndex("Type", "TopParentId", "Id"); - - b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); - - b.HasIndex("Type", "TopParentId", "StartDate"); - - b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); - - b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); - - b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); - - b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); - - b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); - - b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); - - b.ToTable("BaseItems"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Blurhash") - .HasColumnType("bytea"); - - b.Property("DateModified") - .HasColumnType("timestamp with time zone"); - - b.Property("Height") - .HasColumnType("integer"); - - b.Property("ImageType") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("Path") - .IsRequired() - .HasColumnType("text"); - - b.Property("Width") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("ItemId"); - - b.ToTable("BaseItemImageInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => - { - b.Property("Id") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.HasKey("Id", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("BaseItemMetadataFields"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("ProviderId") - .HasColumnType("text"); - - b.Property("ProviderValue") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ItemId", "ProviderId"); - - b.HasIndex("ProviderId", "ProviderValue", "ItemId"); - - b.ToTable("BaseItemProviders"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => - { - b.Property("Id") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.HasKey("Id", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("BaseItemTrailerTypes"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("ChapterIndex") - .HasColumnType("integer"); - - b.Property("ImageDateModified") - .HasColumnType("timestamp with time zone"); - - b.Property("ImagePath") - .HasColumnType("text"); - - b.Property("Name") - .HasColumnType("text"); - - b.Property("StartPositionTicks") - .HasColumnType("bigint"); - - b.HasKey("ItemId", "ChapterIndex"); - - b.ToTable("Chapters"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("Value") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client", "Key") - .IsUnique(); - - b.ToTable("CustomItemDisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ChromecastVersion") - .HasColumnType("integer"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DashboardTheme") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("EnableNextVideoInfoOverlay") - .HasColumnType("boolean"); - - b.Property("IndexBy") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("ScrollDirection") - .HasColumnType("integer"); - - b.Property("ShowBackdrop") - .HasColumnType("boolean"); - - b.Property("ShowSidebar") - .HasColumnType("boolean"); - - b.Property("SkipBackwardLength") - .HasColumnType("integer"); - - b.Property("SkipForwardLength") - .HasColumnType("integer"); - - b.Property("TvHome") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client") - .IsUnique(); - - b.ToTable("DisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("DisplayPreferencesId") - .HasColumnType("integer"); - - b.Property("Order") - .HasColumnType("integer"); - - b.Property("Type") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("DisplayPreferencesId"); - - b.ToTable("HomeSection"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("LastModified") - .HasColumnType("timestamp with time zone"); - - b.Property("Path") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("character varying(512)"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("ImageInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("IndexBy") - .HasColumnType("integer"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("RememberIndexing") - .HasColumnType("boolean"); - - b.Property("RememberSorting") - .HasColumnType("boolean"); - - b.Property("SortBy") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("ViewType") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("ItemDisplayPreferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => - { - b.Property("ItemValueId") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CleanValue") - .IsRequired() - .HasColumnType("text"); - - b.Property("Type") - .HasColumnType("integer"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("ItemValueId"); - - b.HasIndex("Type", "CleanValue") - .IsUnique(); - - b.ToTable("ItemValues"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => - { - b.Property("ItemValueId") - .HasColumnType("uuid"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.HasKey("ItemValueId", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("ItemValuesMap"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("EndTicks") - .HasColumnType("bigint"); - - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("SegmentProviderId") - .IsRequired() - .HasColumnType("text"); - - b.Property("StartTicks") - .HasColumnType("bigint"); - - b.Property("Type") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.ToTable("MediaSegments"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("StreamIndex") - .HasColumnType("integer"); - - b.Property("AspectRatio") - .HasColumnType("text"); - - b.Property("AverageFrameRate") - .HasColumnType("real"); - - b.Property("BitDepth") - .HasColumnType("integer"); - - b.Property("BitRate") - .HasColumnType("integer"); - - b.Property("BlPresentFlag") - .HasColumnType("integer"); - - b.Property("ChannelLayout") - .HasColumnType("text"); - - b.Property("Channels") - .HasColumnType("integer"); - - b.Property("Codec") - .HasColumnType("text"); - - b.Property("CodecTag") - .HasColumnType("text"); - - b.Property("CodecTimeBase") - .HasColumnType("text"); - - b.Property("ColorPrimaries") - .HasColumnType("text"); - - b.Property("ColorSpace") - .HasColumnType("text"); - - b.Property("ColorTransfer") - .HasColumnType("text"); - - b.Property("Comment") - .HasColumnType("text"); - - b.Property("DvBlSignalCompatibilityId") - .HasColumnType("integer"); - - b.Property("DvLevel") - .HasColumnType("integer"); - - b.Property("DvProfile") - .HasColumnType("integer"); - - b.Property("DvVersionMajor") - .HasColumnType("integer"); - - b.Property("DvVersionMinor") - .HasColumnType("integer"); - - b.Property("ElPresentFlag") - .HasColumnType("integer"); - - b.Property("Height") - .HasColumnType("integer"); - - b.Property("IsAnamorphic") - .HasColumnType("boolean"); - - b.Property("IsAvc") - .HasColumnType("boolean"); - - b.Property("IsDefault") - .HasColumnType("boolean"); - - b.Property("IsExternal") - .HasColumnType("boolean"); - - b.Property("IsForced") - .HasColumnType("boolean"); - - b.Property("IsHearingImpaired") - .HasColumnType("boolean"); - - b.Property("IsInterlaced") - .HasColumnType("boolean"); - - b.Property("KeyFrames") - .HasColumnType("text"); - - b.Property("Language") - .HasColumnType("text"); - - b.Property("Level") - .HasColumnType("real"); - - b.Property("NalLengthSize") - .HasColumnType("text"); - - b.Property("Path") - .HasColumnType("text"); - - b.Property("PixelFormat") - .HasColumnType("text"); - - b.Property("Profile") - .HasColumnType("text"); - - b.Property("RealFrameRate") - .HasColumnType("real"); - - b.Property("RefFrames") - .HasColumnType("integer"); - - b.Property("Rotation") - .HasColumnType("integer"); - - b.Property("RpuPresentFlag") - .HasColumnType("integer"); - - b.Property("SampleRate") - .HasColumnType("integer"); - - b.Property("StreamType") - .HasColumnType("integer"); - - b.Property("TimeBase") - .HasColumnType("text"); - - b.Property("Title") - .HasColumnType("text"); - - b.Property("Width") - .HasColumnType("integer"); - - b.HasKey("ItemId", "StreamIndex"); - - b.HasIndex("StreamIndex"); - - b.HasIndex("StreamType"); - - b.HasIndex("StreamIndex", "StreamType"); - - b.HasIndex("StreamIndex", "StreamType", "Language"); - - b.ToTable("MediaStreamInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.People", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("PersonType") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.ToTable("Peoples"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("PeopleId") - .HasColumnType("uuid"); - - b.Property("ListOrder") - .HasColumnType("integer"); - - b.Property("Role") - .HasColumnType("text"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("ItemId", "PeopleId"); - - b.HasIndex("PeopleId"); - - b.HasIndex("ItemId", "ListOrder"); - - b.HasIndex("ItemId", "SortOrder"); - - b.ToTable("PeopleBaseItemMap"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Kind") - .HasColumnType("integer"); - - b.Property("Permission_Permissions_Guid") - .HasColumnType("uuid"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("bigint"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("Value") - .HasColumnType("boolean"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Permissions"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Kind") - .HasColumnType("integer"); - - b.Property("Preference_Preferences_Guid") - .HasColumnType("uuid"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("bigint"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(65535) - .HasColumnType("character varying(65535)"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Preferences"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("text"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastActivity") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.HasKey("Id"); - - b.HasIndex("AccessToken") - .IsUnique(); - - b.ToTable("ApiKeys"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("text"); - - b.Property("AppName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("AppVersion") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DateCreated") - .HasColumnType("timestamp with time zone"); - - b.Property("DateLastActivity") - .HasColumnType("timestamp with time zone"); - - b.Property("DateModified") - .HasColumnType("timestamp with time zone"); - - b.Property("DeviceId") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.Property("DeviceName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId"); - - b.HasIndex("AccessToken", "DateLastActivity"); - - b.HasIndex("DeviceId", "DateLastActivity"); - - b.HasIndex("UserId", "DeviceId"); - - b.ToTable("Devices"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CustomName") - .HasColumnType("text"); - - b.Property("DeviceId") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId") - .IsUnique(); - - b.ToTable("DeviceOptions"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("Width") - .HasColumnType("integer"); - - b.Property("Bandwidth") - .HasColumnType("integer"); - - b.Property("Height") - .HasColumnType("integer"); - - b.Property("Interval") - .HasColumnType("integer"); - - b.Property("ThumbnailCount") - .HasColumnType("integer"); - - b.Property("TileHeight") - .HasColumnType("integer"); - - b.Property("TileWidth") - .HasColumnType("integer"); - - b.HasKey("ItemId", "Width"); - - b.ToTable("TrickplayInfos"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("AudioLanguagePreference") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("AuthenticationProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("CastReceiverId") - .HasMaxLength(32) - .HasColumnType("character varying(32)"); - - b.Property("DisplayCollectionsView") - .HasColumnType("boolean"); - - b.Property("DisplayMissingEpisodes") - .HasColumnType("boolean"); - - b.Property("EnableAutoLogin") - .HasColumnType("boolean"); - - b.Property("EnableLocalPassword") - .HasColumnType("boolean"); - - b.Property("EnableNextEpisodeAutoPlay") - .HasColumnType("boolean"); - - b.Property("EnableUserPreferenceAccess") - .HasColumnType("boolean"); - - b.Property("HidePlayedInLatest") - .HasColumnType("boolean"); - - b.Property("InternalId") - .HasColumnType("bigint"); - - b.Property("InvalidLoginAttemptCount") - .HasColumnType("integer"); - - b.Property("LastActivityDate") - .HasColumnType("timestamp with time zone"); - - b.Property("LastLoginDate") - .HasColumnType("timestamp with time zone"); - - b.Property("LoginAttemptsBeforeLockout") - .HasColumnType("integer"); - - b.Property("MaxActiveSessions") - .HasColumnType("integer"); - - b.Property("MaxParentalAgeRating") - .HasColumnType("integer"); - - b.Property("MustUpdatePassword") - .HasColumnType("boolean"); - - b.Property("Password") - .HasMaxLength(65535) - .HasColumnType("character varying(65535)"); - - b.Property("PasswordResetProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("PlayDefaultAudioTrack") - .HasColumnType("boolean"); - - b.Property("RememberAudioSelections") - .HasColumnType("boolean"); - - b.Property("RememberSubtitleSelections") - .HasColumnType("boolean"); - - b.Property("RemoteClientBitrateLimit") - .HasColumnType("integer"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("bigint"); - - b.Property("SubtitleLanguagePreference") - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.Property("SubtitleMode") - .HasColumnType("integer"); - - b.Property("SyncPlayAccess") - .HasColumnType("integer"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("character varying(255)"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => - { - b.Property("ItemId") - .HasColumnType("uuid"); - - b.Property("UserId") - .HasColumnType("uuid"); - - b.Property("CustomDataKey") - .HasColumnType("text"); - - b.Property("AudioStreamIndex") - .HasColumnType("integer"); - - b.Property("IsFavorite") - .HasColumnType("boolean"); - - b.Property("LastPlayedDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Likes") - .HasColumnType("boolean"); - - b.Property("PlayCount") - .HasColumnType("integer"); - - b.Property("PlaybackPositionTicks") - .HasColumnType("bigint"); - - b.Property("Played") - .HasColumnType("boolean"); - - b.Property("Rating") - .HasColumnType("double precision"); - - b.Property("SubtitleStreamIndex") - .HasColumnType("integer"); - - b.HasKey("ItemId", "UserId", "CustomDataKey"); - - b.HasIndex("UserId"); - - b.HasIndex("ItemId", "UserId", "IsFavorite"); - - b.HasIndex("ItemId", "UserId", "LastPlayedDate"); - - b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); - - b.HasIndex("ItemId", "UserId", "Played"); - - b.ToTable("UserData"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("AccessSchedules") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Children") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem") - .WithMany("ParentAncestors") - .HasForeignKey("ParentItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("ParentItem"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany() - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Images") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("LockedFields") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Provider") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("TrailerTypes") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Chapters") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("DisplayPreferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => - { - b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null) - .WithMany("HomeSections") - .HasForeignKey("DisplayPreferencesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithOne("ProfileImage") - .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("ItemDisplayPreferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("ItemValues") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue") - .WithMany("BaseItemsMap") - .HasForeignKey("ItemValueId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("ItemValue"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("MediaStreams") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("Peoples") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Data.Entities.People", "People") - .WithMany("BaseItems") - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("People"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Permissions") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => - { - b.HasOne("Jellyfin.Data.Entities.User", null) - .WithMany("Preferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => - { - b.HasOne("Jellyfin.Data.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => - { - b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item") - .WithMany("UserData") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Data.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => - { - b.Navigation("Chapters"); - - b.Navigation("Children"); - - b.Navigation("Images"); - - b.Navigation("ItemValues"); - - b.Navigation("LockedFields"); - - b.Navigation("MediaStreams"); - - b.Navigation("ParentAncestors"); - - b.Navigation("Peoples"); - - b.Navigation("Provider"); - - b.Navigation("TrailerTypes"); - - b.Navigation("UserData"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => - { - b.Navigation("HomeSections"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => - { - b.Navigation("BaseItemsMap"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.People", b => - { - b.Navigation("BaseItems"); - }); - - modelBuilder.Entity("Jellyfin.Data.Entities.User", b => - { - b.Navigation("AccessSchedules"); - - b.Navigation("DisplayPreferences"); - - b.Navigation("ItemDisplayPreferences"); - - b.Navigation("Permissions"); - - b.Navigation("Preferences"); - - b.Navigation("ProfileImage"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/PgSqlDesignTimeJellyfinDbFactory.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/PgSqlDesignTimeJellyfinDbFactory.cs deleted file mode 100644 index bf949d570b..0000000000 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/PgSqlDesignTimeJellyfinDbFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Jellyfin.Server.Implementations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Jellyfin.Database.Providers.PgSql -{ - /// - /// The design time factory for . - /// This is only used for the creation of migrations and not during runtime. - /// - internal sealed class PgSqlDesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory - { - public JellyfinDbContext CreateDbContext(string[] args) - { - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(f => f.MigrationsAssembly(GetType().Assembly)); - - return new JellyfinDbContext( - optionsBuilder.Options, - NullLogger.Instance, - new PgSqlDatabaseProvider(null!, NullLogger.Instance)); - } - } -} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs deleted file mode 100644 index ccaf38d2ac..0000000000 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/PgSqlDatabaseProvider.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Diagnostics; -using System.Reflection; -using Jellyfin.Server.Implementations; -using Jellyfin.Server.Implementations.DatabaseConfiguration; -using MediaBrowser.Common.Configuration; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Npgsql; - -namespace Jellyfin.Database.Providers.PgSql; - -/// -/// Configures jellyfin to use an Postgres database. -/// -[JellyfinDatabaseProviderKey("Jellyfin-PgSql")] -public sealed class PgSqlDatabaseProvider : IJellyfinDatabaseProvider -{ - private readonly IConfigurationManager _configurationManager; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// Configuration manager to get PgSQL connection data. - /// A logger. - public PgSqlDatabaseProvider(IConfigurationManager configurationManager, ILogger logger) - { - _configurationManager = configurationManager; - _logger = logger; - } - - /// - public IDbContextFactory? DbContextFactory { get; set; } - - /// - public void Initialise(DbContextOptionsBuilder options) - { - var dbSettings = _configurationManager.GetConfiguration("database"); - - if (dbSettings.PostgreSql is null) - { - throw new InvalidOperationException("Selected PgSQL as database provider but did not provide required configuration. Please see docs."); - } - - var connectionBuilder = new NpgsqlConnectionStringBuilder(); - connectionBuilder.ApplicationName = $"jellyfin+{FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly()!.Location).FileVersion}"; - connectionBuilder.CommandTimeout = dbSettings.PostgreSql.Timeout; - connectionBuilder.Database = dbSettings.PostgreSql.DatabaseName; - connectionBuilder.Username = dbSettings.PostgreSql.Username; - connectionBuilder.Password = dbSettings.PostgreSql.Password; - connectionBuilder.Host = dbSettings.PostgreSql.ServerName; - connectionBuilder.Port = dbSettings.PostgreSql.Port; - - var connectionString = connectionBuilder.ToString(); - - options - .UseNpgsql(connectionString, pgSqlOptions => pgSqlOptions.MigrationsAssembly(GetType().Assembly)); - } - - /// - public Task RunScheduledOptimisation(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - /// - public void OnModelCreating(ModelBuilder modelBuilder) - { - } - - /// - public Task RunShutdownTask(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } -} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Properties/AssemblyInfo.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Properties/AssemblyInfo.cs deleted file mode 100644 index 27022c7de8..0000000000 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Jellyfin.Database.Providers.PgSql")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: NeutralResourcesLanguage("en")] -[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index 1dd7cfe045..9bf67118d0 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; -using Jellyfin.Database.Providers.PgSql; using Jellyfin.Database.Providers.SqLite; using Jellyfin.Server.Implementations.DatabaseConfiguration; using MediaBrowser.Common.Configuration; @@ -23,7 +20,6 @@ public static class ServiceCollectionExtensions private static IEnumerable DatabaseProviderTypes() { yield return typeof(SqliteDatabaseProvider); - yield return typeof(PgSqlDatabaseProvider); } private static IDictionary GetSupportedDbProviders() diff --git a/Jellyfin.sln b/Jellyfin.sln index e6642c296d..5bc1b6ef8a 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -89,9 +89,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.LiveTv", "src\Jell EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jellyfin.Database", "Jellyfin.Database", "{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.SqLite", "Jellyfin.Database\Jellyfin.Database.Providers.SqLite\Jellyfin.Database.Providers.SqLite.csproj", "{A5590358-33CC-4B39-BDE7-DC62FEB03C76}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.PgSql", "Jellyfin.Database\Jellyfin.Database.Providers.PgSql\Jellyfin.Database.Providers.PgSql.csproj", "{EC91A604-C99E-44E2-BB74-B4EB2A4B6A0C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Providers.SqLite", "Jellyfin.Database\Jellyfin.Database.Providers.SqLite\Jellyfin.Database.Providers.SqLite.csproj", "{A5590358-33CC-4B39-BDE7-DC62FEB03C76}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Implementations", "Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj", "{8C9F9221-8415-496C-B1F5-E7756F03FA59}" EndProject @@ -253,10 +251,6 @@ Global {A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5590358-33CC-4B39-BDE7-DC62FEB03C76}.Release|Any CPU.Build.0 = Release|Any CPU - {EC91A604-C99E-44E2-BB74-B4EB2A4B6A0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EC91A604-C99E-44E2-BB74-B4EB2A4B6A0C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EC91A604-C99E-44E2-BB74-B4EB2A4B6A0C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EC91A604-C99E-44E2-BB74-B4EB2A4B6A0C}.Release|Any CPU.Build.0 = Release|Any CPU {8C9F9221-8415-496C-B1F5-E7756F03FA59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8C9F9221-8415-496C-B1F5-E7756F03FA59}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C9F9221-8415-496C-B1F5-E7756F03FA59}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -290,7 +284,6 @@ Global {C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {8C6B2B13-58A4-4506-9DAB-1F882A093FE0} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} {A5590358-33CC-4B39-BDE7-DC62FEB03C76} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} - {EC91A604-C99E-44E2-BB74-B4EB2A4B6A0C} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} {8C9F9221-8415-496C-B1F5-E7756F03FA59} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs b/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs index 78e431be14..3fe2caca79 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading.Tasks; -using Jellyfin.Database.Providers.PgSql; using Jellyfin.Server.Implementations.Migrations; using Microsoft.EntityFrameworkCore; using Xunit; @@ -9,14 +6,6 @@ namespace Jellyfin.Server.Implementations.Tests.EfMigrations; public class EfMigrationTests { - [Fact] - public void CheckForUnappliedMigrations_PgSQL() - { - var dbDesignContext = new PgSqlDesignTimeJellyfinDbFactory(); - var context = dbDesignContext.CreateDbContext([]); - Assert.False(context.Database.HasPendingModelChanges(), "There are unapplied changes to the EFCore model for PgSQL. Please create a Migration."); - } - [Fact] public void CheckForUnappliedMigrations_SqLite() { From f07e1f4aaee9b61b07d1389107973ead146c639b Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 19 Feb 2025 18:30:18 +0000 Subject: [PATCH 035/508] Reverted Comparison code for name check --- Jellyfin.Api/Controllers/UserController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 88e5d46adc..838578fab8 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -390,7 +390,7 @@ public class UserController : BaseJellyfinApiController return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed."); } - if (!string.Equals(user.Username, updateUser.Name, StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) { await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false); } From 5303445c9b4c9934145151f20c084033ffd1e7c6 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 7 Dec 2024 19:26:58 +0100 Subject: [PATCH 036/508] Migrate to IExternalUrlProvider --- .../Providers/IExternalId.cs | 6 -- .../Providers/ExternalIdInfo.cs | 14 +-- .../Manager/ProviderManager.cs | 32 +----- .../Movies/ImdbExternalId.cs | 3 - .../Movies/ImdbExternalUrlProvider.cs | 27 +++++ .../Movies/ImdbPersonExternalId.cs | 3 - MediaBrowser.Providers/Music/ImvdbId.cs | 3 - .../Plugins/AudioDb/AudioDbAlbumExternalId.cs | 3 - .../AudioDbAlbumExternalUrlProvider.cs | 32 ++++++ .../AudioDb/AudioDbArtistExternalId.cs | 3 - .../AudioDbArtistExternalUrlProvider.cs | 33 ++++++ .../AudioDb/AudioDbOtherAlbumExternalId.cs | 3 - .../AudioDb/AudioDbOtherArtistExternalId.cs | 3 - .../MusicBrainzAlbumArtistExternalId.cs | 3 - ...sicBrainzAlbumArtistExternalUrlProvider.cs | 29 +++++ .../MusicBrainz/MusicBrainzAlbumExternalId.cs | 3 - .../MusicBrainzAlbumExternalUrlProvider.cs | 29 +++++ .../MusicBrainzArtistExternalId.cs | 3 - .../MusicBrainzArtistExternalUrlProvider.cs | 33 ++++++ .../MusicBrainzOtherArtistExternalId.cs | 3 - .../MusicBrainzReleaseGroupExternalId.cs | 3 - ...icBrainzReleaseGroupExternalUrlProvider.cs | 29 +++++ .../MusicBrainzTrackExternalUrlProvider.cs | 29 +++++ .../Plugins/MusicBrainz/MusicBrainzTrackId.cs | 3 - .../Tmdb/BoxSets/TmdbBoxSetExternalId.cs | 3 - .../Tmdb/Movies/TmdbMovieExternalId.cs | 3 - .../Tmdb/People/TmdbPersonExternalId.cs | 3 - .../Plugins/Tmdb/TV/TmdbSeriesExternalId.cs | 3 - .../Plugins/Tmdb/TmdbExternalUrlProvider.cs | 101 ++++++++++++++++++ MediaBrowser.Providers/TV/Zap2ItExternalId.cs | 3 - .../TV/Zap2ItExternalUrlProvider.cs | 25 +++++ .../Parsers/EpisodeNfoProviderTests.cs | 2 +- .../Parsers/MovieNfoParserTests.cs | 2 +- .../Parsers/MusicAlbumNfoProviderTests.cs | 2 +- .../Parsers/MusicArtistNfoParserTests.cs | 2 +- 35 files changed, 374 insertions(+), 107 deletions(-) create mode 100644 MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs index f451eac6dd..584c3297a9 100644 --- a/MediaBrowser.Controller/Providers/IExternalId.cs +++ b/MediaBrowser.Controller/Providers/IExternalId.cs @@ -31,12 +31,6 @@ namespace MediaBrowser.Controller.Providers /// ExternalIdMediaType? Type { get; } - /// - /// Gets the URL format string for this id. - /// - [Obsolete("Obsolete in 10.10, to be removed in 10.11")] - string? UrlFormatString { get; } - /// /// Determines whether this id supports a given item type. /// diff --git a/MediaBrowser.Model/Providers/ExternalIdInfo.cs b/MediaBrowser.Model/Providers/ExternalIdInfo.cs index 1f5163aa8e..e7a3099243 100644 --- a/MediaBrowser.Model/Providers/ExternalIdInfo.cs +++ b/MediaBrowser.Model/Providers/ExternalIdInfo.cs @@ -1,5 +1,3 @@ -using System; - namespace MediaBrowser.Model.Providers { /// @@ -13,15 +11,11 @@ namespace MediaBrowser.Model.Providers /// Name of the external id provider (IE: IMDB, MusicBrainz, etc). /// Key for this id. This key should be unique across all providers. /// Specific media type for this id. - /// URL format string. - public ExternalIdInfo(string name, string key, ExternalIdMediaType? type, string? urlFormatString) + public ExternalIdInfo(string name, string key, ExternalIdMediaType? type) { Name = name; Key = key; Type = type; -#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11 - UrlFormatString = urlFormatString; -#pragma warning restore CS0618 // Type or member is obsolete } /// @@ -46,11 +40,5 @@ namespace MediaBrowser.Model.Providers /// This can be used along with the to localize the external id on the client. /// public ExternalIdMediaType? Type { get; set; } - - /// - /// Gets or sets the URL format string. - /// - [Obsolete("Obsolete in 10.10, to be removed in 10.11")] - public string? UrlFormatString { get; set; } } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 8c45abe252..856f33b497 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -899,35 +899,10 @@ namespace MediaBrowser.Providers.Manager /// public IEnumerable GetExternalUrls(BaseItem item) { -#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11 - var legacyExternalIdUrls = GetExternalIds(item) - .Select(i => - { - var urlFormatString = i.UrlFormatString; - if (string.IsNullOrEmpty(urlFormatString) - || !item.TryGetProviderId(i.Key, out var providerId)) - { - return null; - } - - return new ExternalUrl - { - Name = i.ProviderName, - Url = string.Format( - CultureInfo.InvariantCulture, - urlFormatString, - providerId) - }; - }) - .OfType(); -#pragma warning restore CS0618 // Type or member is obsolete - - var externalUrls = _externalUrlProviders + return _externalUrlProviders .SelectMany(p => p .GetExternalUrls(item) .Select(externalUrl => new ExternalUrl { Name = p.Name, Url = externalUrl })); - - return legacyExternalIdUrls.Concat(externalUrls).OrderBy(u => u.Name); } /// @@ -937,10 +912,7 @@ namespace MediaBrowser.Providers.Manager .Select(i => new ExternalIdInfo( name: i.ProviderName, key: i.Key, - type: i.Type, -#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11 - urlFormatString: i.UrlFormatString)); -#pragma warning restore CS0618 // Type or member is obsolete + type: i.Type)); } /// diff --git a/MediaBrowser.Providers/Movies/ImdbExternalId.cs b/MediaBrowser.Providers/Movies/ImdbExternalId.cs index a8d74aa0b5..def0b13c07 100644 --- a/MediaBrowser.Providers/Movies/ImdbExternalId.cs +++ b/MediaBrowser.Providers/Movies/ImdbExternalId.cs @@ -21,9 +21,6 @@ namespace MediaBrowser.Providers.Movies /// public ExternalIdMediaType? Type => null; - /// - public string UrlFormatString => "https://www.imdb.com/title/{0}"; - /// public bool Supports(IHasProviderIds item) { diff --git a/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs new file mode 100644 index 0000000000..eadcc976af --- /dev/null +++ b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Movies; + +/// +/// External URLs for IMDb. +/// +public class ImdbExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "IMDb"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + var baseUrl = "https://www.imdb.com/"; + var externalId = item.GetProviderId(MetadataProvider.Imdb); + + if (!string.IsNullOrEmpty(externalId)) + { + yield return baseUrl + $"title/{externalId}"; + } + } +} diff --git a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs index 8151ab4715..aa2b2fae95 100644 --- a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs +++ b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Movies /// public ExternalIdMediaType? Type => ExternalIdMediaType.Person; - /// - public string UrlFormatString => "https://www.imdb.com/name/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Person; } diff --git a/MediaBrowser.Providers/Music/ImvdbId.cs b/MediaBrowser.Providers/Music/ImvdbId.cs index ed69f369c0..b2c0b7019e 100644 --- a/MediaBrowser.Providers/Music/ImvdbId.cs +++ b/MediaBrowser.Providers/Music/ImvdbId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Music /// public ExternalIdMediaType? Type => null; - /// - public string? UrlFormatString => null; - /// public bool Supports(IHasProviderIds item) => item is MusicVideo; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs index 138cfef19a..622bb1dba4 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public ExternalIdMediaType? Type => null; - /// - public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; - /// public bool Supports(IHasProviderIds item) => item is MusicAlbum; } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs new file mode 100644 index 0000000000..1615f1ce59 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.AudioDb; + +/// +/// External artist URLs for AudioDb. +/// +public class AudioDbAlbumExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "TheAudioDb Album"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + var externalId = item.GetProviderId(MetadataProvider.AudioDbAlbum); + if (!string.IsNullOrEmpty(externalId)) + { + var baseUrl = "https://www.theaudiodb.com/"; + switch (item) + { + case MusicAlbum: + yield return baseUrl + $"album/{externalId}"; + break; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs index 8aceb48c0c..3b5955b5be 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; - /// - public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; - /// public bool Supports(IHasProviderIds item) => item is MusicArtist; } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs new file mode 100644 index 0000000000..5c5057fa1a --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.AudioDb; + +/// +/// External artist URLs for AudioDb. +/// +public class AudioDbArtistExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "TheAudioDb Artist"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + var externalId = item.GetProviderId(MetadataProvider.AudioDbArtist); + if (!string.IsNullOrEmpty(externalId)) + { + var baseUrl = "https://www.theaudiodb.com/"; + switch (item) + { + case MusicAlbum: + case Person: + yield return baseUrl + $"artist/{externalId}"; + break; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs index 014481da24..fdfd330cd3 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public ExternalIdMediaType? Type => ExternalIdMediaType.Album; - /// - public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio; } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs index 7875391043..5a39ec1cd9 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; - /// - public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs index 825fe32fa2..f1fc4a137b 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs @@ -19,9 +19,6 @@ public class MusicBrainzAlbumArtistExternalId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs new file mode 100644 index 0000000000..3de18f4ccf --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// +/// External album artist URLs for MusicBrainz. +/// +public class MusicBrainzAlbumArtistExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "MusicBrainz Album Artist"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + if (item is MusicAlbum) + { + var externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist); + if (!string.IsNullOrEmpty(externalId)) + { + yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}"; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs index b7d53984c5..48784e0ecd 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs @@ -19,9 +19,6 @@ public class MusicBrainzAlbumExternalId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.Album; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs new file mode 100644 index 0000000000..6d0afdd508 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// +/// External album URLs for MusicBrainz. +/// +public class MusicBrainzAlbumExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "MusicBrainz Album"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + if (item is MusicAlbum) + { + var externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); + if (!string.IsNullOrEmpty(externalId)) + { + yield return Plugin.Instance!.Configuration.Server + $"/release/{externalId}"; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs index b3f001618d..bd5d67ed1b 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs @@ -19,9 +19,6 @@ public class MusicBrainzArtistExternalId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; - /// public bool Supports(IHasProviderIds item) => item is MusicArtist; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs new file mode 100644 index 0000000000..cd71191bff --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// +/// External artist URLs for MusicBrainz. +/// +public class MusicBrainzArtistExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "MusicBrainz Artist"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + var externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); + if (!string.IsNullOrEmpty(externalId)) + { + switch (item) + { + case MusicAlbum: + case Person: + yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}"; + + break; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs index a0a922293d..470cdad662 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs @@ -19,9 +19,6 @@ public class MusicBrainzOtherArtistExternalId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs index 47b6d69633..c19b62abfe 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs @@ -19,9 +19,6 @@ public class MusicBrainzReleaseGroupExternalId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs new file mode 100644 index 0000000000..9bc0103794 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// +/// External release group URLs for MusicBrainz. +/// +public class MusicBrainzReleaseGroupExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "MusicBrainz Release Group"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + if (item is MusicAlbum) + { + var externalId = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); + if (!string.IsNullOrEmpty(externalId)) + { + yield return Plugin.Instance!.Configuration.Server + $"/release-group/{externalId}"; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs new file mode 100644 index 0000000000..fc26dc54df --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// +/// External track URLs for MusicBrainz. +/// +public class MusicBrainzTrackExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "MusicBrainz Track"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + if (item is Audio) + { + var externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); + if (!string.IsNullOrEmpty(externalId)) + { + yield return Plugin.Instance!.Configuration.Server + $"/track/{externalId}"; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs index cb4345660d..6a7b6f5412 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs @@ -19,9 +19,6 @@ public class MusicBrainzTrackId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.Track; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs index d453a4ff44..2076589d34 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs @@ -20,9 +20,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets /// public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet; - /// - public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}"; - /// public bool Supports(IHasProviderIds item) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs index 6d6032e8f6..9a1d872ec2 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs @@ -20,9 +20,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies /// public ExternalIdMediaType? Type => ExternalIdMediaType.Movie; - /// - public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}"; - /// public bool Supports(IHasProviderIds item) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs index d26a70028c..2c0787b15d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs @@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People /// public ExternalIdMediaType? Type => ExternalIdMediaType.Person; - /// - public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}"; - /// public bool Supports(IHasProviderIds item) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs index 5f2d7909a9..840cec9841 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs @@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV /// public ExternalIdMediaType? Type => ExternalIdMediaType.Series; - /// - public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}"; - /// public bool Supports(IHasProviderIds item) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs new file mode 100644 index 0000000000..b8fd18f286 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using TMDbLib.Objects.TvShows; + +namespace MediaBrowser.Providers.Plugins.Tmdb; + +/// +/// External URLs for TMDb. +/// +public class TmdbExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "TMDB"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + switch (item) + { + case Series: + var externalId = item.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(externalId)) + { + yield return TmdbUtils.BaseTmdbUrl + $"tv/{externalId}"; + } + + break; + case Season season: + var seriesExternalId = season.Series.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(seriesExternalId)) + { + var orderString = season.Series.DisplayOrder; + if (string.IsNullOrEmpty(orderString)) + { + // Default order is airdate + yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{season.IndexNumber}"; + } + + if (Enum.TryParse(season.Series.DisplayOrder, out var order)) + { + if (order.Equals(TvGroupType.OriginalAirDate)) + { + yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{season.IndexNumber}"; + } + } + } + + break; + case Episode episode: + seriesExternalId = episode.Series.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(seriesExternalId)) + { + var orderString = episode.Series.DisplayOrder; + if (string.IsNullOrEmpty(orderString)) + { + // Default order is airdate + yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{episode.Season.IndexNumber}/episode/{episode.IndexNumber}"; + } + + if (Enum.TryParse(orderString, out var order)) + { + if (order.Equals(TvGroupType.OriginalAirDate)) + { + yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{episode.Season.IndexNumber}/episode/{episode.IndexNumber}"; + } + } + } + + break; + case Movie: + externalId = item.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(externalId)) + { + yield return TmdbUtils.BaseTmdbUrl + $"movie/{externalId}"; + } + + break; + case Person: + externalId = item.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(externalId)) + { + yield return TmdbUtils.BaseTmdbUrl + $"person/{externalId}"; + } + + break; + case BoxSet: + externalId = item.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(externalId)) + { + yield return TmdbUtils.BaseTmdbUrl + $"collection/{externalId}"; + } + + break; + } + } +} diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs index 3cb18e4248..8907d7744a 100644 --- a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs +++ b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.TV /// public ExternalIdMediaType? Type => null; - /// - public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}"; - /// public bool Supports(IHasProviderIds item) => item is Series; } diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs new file mode 100644 index 0000000000..f6516fddeb --- /dev/null +++ b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.TV; + +/// +/// External URLs for TMDb. +/// +public class Zap2ItExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "Zap2It"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + var externalId = item.GetProviderId(MetadataProvider.Zap2It); + if (!string.IsNullOrEmpty(externalId)) + { + yield return $"http://tvlistings.zap2it.com/overview.html?programSeriesId={externalId}"; + } + } +} diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs index f9126ce9bb..a04b37f215 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs @@ -26,7 +26,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers var providerManager = new Mock(); var imdbExternalId = new ImdbExternalId(); - var externalIdInfo = new ExternalIdInfo(imdbExternalId.ProviderName, imdbExternalId.Key, imdbExternalId.Type, imdbExternalId.UrlFormatString); + var externalIdInfo = new ExternalIdInfo(imdbExternalId.ProviderName, imdbExternalId.Key, imdbExternalId.Type); providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny())) .Returns(new[] { externalIdInfo }); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index 9c2655154d..a71a08d8cd 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -34,7 +34,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers var providerManager = new Mock(); var tmdbExternalId = new TmdbMovieExternalId(); - var externalIdInfo = new ExternalIdInfo(tmdbExternalId.ProviderName, tmdbExternalId.Key, tmdbExternalId.Type, tmdbExternalId.UrlFormatString); + var externalIdInfo = new ExternalIdInfo(tmdbExternalId.ProviderName, tmdbExternalId.Key, tmdbExternalId.Type); providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny())) .Returns(new[] { externalIdInfo }); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs index f815dfaa9a..24e9b9feeb 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs @@ -24,7 +24,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers var providerManager = new Mock(); var musicBrainzArtist = new MusicBrainzArtistExternalId(); - var externalIdInfo = new ExternalIdInfo(musicBrainzArtist.ProviderName, musicBrainzArtist.Key, musicBrainzArtist.Type, "MusicBrainzServer"); + var externalIdInfo = new ExternalIdInfo(musicBrainzArtist.ProviderName, musicBrainzArtist.Key, musicBrainzArtist.Type); providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny())) .Returns(new[] { externalIdInfo }); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs index 78183d9ffd..4d1956bde7 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs @@ -24,7 +24,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers var providerManager = new Mock(); var musicBrainzArtist = new MusicBrainzArtistExternalId(); - var externalIdInfo = new ExternalIdInfo(musicBrainzArtist.ProviderName, musicBrainzArtist.Key, musicBrainzArtist.Type, "MusicBrainzServer"); + var externalIdInfo = new ExternalIdInfo(musicBrainzArtist.ProviderName, musicBrainzArtist.Key, musicBrainzArtist.Type); providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny())) .Returns(new[] { externalIdInfo }); From 44dfe554a894561d3878c8f204d989e4d5a72d72 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 20 Feb 2025 09:55:02 +0000 Subject: [PATCH 037/508] Moved Database projects under /src removed old pgsql references --- .devcontainer/pgsql/Dockerfile | 8 --- .devcontainer/pgsql/devcontainer.json | 47 ---------------- .devcontainer/pgsql/docker-compose.yaml | 48 ---------------- .gitignore | 2 - Jellyfin.Data/Jellyfin.Data.csproj | 2 +- .../DbConfiguration/PostgreSqlOptions.cs | 39 ------------- .../Jellyfin.Database.Providers.SqLite.csproj | 55 ------------------- .../Jellyfin.Server.Implementations.csproj | 5 +- Jellyfin.sln | 7 ++- .../DatabaseConfigurationOptions.cs | 5 -- .../Entities/AccessSchedule.cs | 0 .../Entities/ActivityLog.cs | 0 .../Entities/AncestorId.cs | 0 .../Entities/AttachmentStreamInfo.cs | 0 .../Entities/BaseItemEntity.cs | 0 .../Entities/BaseItemExtraType.cs | 0 .../Entities/BaseItemImageInfo.cs | 0 .../Entities/BaseItemMetadataField.cs | 0 .../Entities/BaseItemProvider.cs | 0 .../Entities/BaseItemTrailerType.cs | 0 .../Entities/Chapter.cs | 0 .../Entities/CustomItemDisplayPreferences.cs | 0 .../Entities/DisplayPreferences.cs | 0 .../Entities/Group.cs | 0 .../Entities/HomeSection.cs | 0 .../Entities/ImageInfo.cs | 0 .../Entities/ImageInfoImageType.cs | 0 .../Entities/ItemDisplayPreferences.cs | 0 .../Entities/ItemValue.cs | 0 .../Entities/ItemValueMap.cs | 0 .../Entities/ItemValueType.cs | 0 .../Entities/Libraries/Artwork.cs | 0 .../Entities/Libraries/Book.cs | 0 .../Entities/Libraries/BookMetadata.cs | 0 .../Entities/Libraries/Chapter.cs | 0 .../Entities/Libraries/Collection.cs | 0 .../Entities/Libraries/CollectionItem.cs | 0 .../Entities/Libraries/Company.cs | 0 .../Entities/Libraries/CompanyMetadata.cs | 0 .../Entities/Libraries/CustomItem.cs | 0 .../Entities/Libraries/CustomItemMetadata.cs | 0 .../Entities/Libraries/Episode.cs | 0 .../Entities/Libraries/EpisodeMetadata.cs | 0 .../Entities/Libraries/Genre.cs | 0 .../Entities/Libraries/ItemMetadata.cs | 0 .../Entities/Libraries/Library.cs | 0 .../Entities/Libraries/LibraryItem.cs | 0 .../Entities/Libraries/MediaFile.cs | 0 .../Entities/Libraries/MediaFileStream.cs | 0 .../Entities/Libraries/MetadataProvider.cs | 0 .../Entities/Libraries/MetadataProviderId.cs | 0 .../Entities/Libraries/Movie.cs | 0 .../Entities/Libraries/MovieMetadata.cs | 0 .../Entities/Libraries/MusicAlbum.cs | 0 .../Entities/Libraries/MusicAlbumMetadata.cs | 0 .../Entities/Libraries/Person.cs | 0 .../Entities/Libraries/PersonRole.cs | 0 .../Entities/Libraries/Photo.cs | 0 .../Entities/Libraries/PhotoMetadata.cs | 0 .../Entities/Libraries/Rating.cs | 0 .../Entities/Libraries/RatingSource.cs | 0 .../Entities/Libraries/Release.cs | 0 .../Entities/Libraries/Season.cs | 0 .../Entities/Libraries/SeasonMetadata.cs | 0 .../Entities/Libraries/Series.cs | 0 .../Entities/Libraries/SeriesMetadata.cs | 0 .../Entities/Libraries/Track.cs | 0 .../Entities/Libraries/TrackMetadata.cs | 0 .../Entities/MediaSegment.cs | 0 .../Entities/MediaStreamInfo.cs | 0 .../Entities/MediaStreamTypeEntity.cs | 0 .../Entities/People.cs | 0 .../Entities/PeopleBaseItemMap.cs | 0 .../Entities/Permission.cs | 0 .../Entities/Preference.cs | 0 .../Entities/ProgramAudioEntity.cs | 0 .../Entities/Security/ApiKey.cs | 0 .../Entities/Security/Device.cs | 0 .../Entities/Security/DeviceOptions.cs | 0 .../Entities/TrickplayInfo.cs | 0 .../Entities/User.cs | 0 .../Entities/UserData.cs | 0 .../Enums/ArtKind.cs | 0 .../Enums/ChromecastVersion.cs | 0 .../Enums/DynamicDayOfWeek.cs | 0 .../Enums/HomeSectionType.cs | 0 .../Enums/IndexingKind.cs | 0 .../Enums/MediaFileKind.cs | 0 .../Enums/MediaSegmentType.cs | 0 .../Enums/PermissionKind.cs | 0 .../Enums/PersonRoleType.cs | 0 .../Enums/PreferenceKind.cs | 0 .../Enums/ScrollDirection.cs | 0 .../Enums/SortOrder.cs | 0 .../Enums/SubtitlePlaybackMode.cs | 0 .../Enums/SyncPlayUserAccessType.cs | 0 .../Enums/ViewType.cs | 0 .../IJellyfinDatabaseProvider.cs | 0 .../Interfaces/IHasArtwork.cs | 0 .../Interfaces/IHasCompanies.cs | 0 .../Interfaces/IHasConcurrencyToken.cs | 0 .../Interfaces/IHasPermissions.cs | 0 .../Interfaces/IHasReleases.cs | 0 .../Jellyfin.Database.Implementations.csproj | 25 +++++++++ .../JellyfinDatabaseProviderKeyAttribute.cs | 0 .../JellyfinDbContext.cs | 0 .../ActivityLogConfiguration.cs | 0 .../AncestorIdConfiguration.cs | 0 .../ModelConfiguration/ApiKeyConfiguration.cs | 0 .../AttachmentStreamInfoConfiguration.cs | 0 .../BaseItemConfiguration.cs | 0 .../BaseItemMetadataFieldConfiguration.cs | 0 .../BaseItemProviderConfiguration.cs | 0 .../BaseItemTrailerTypeConfiguration.cs | 0 .../ChapterConfiguration.cs | 0 ...stomItemDisplayPreferencesConfiguration.cs | 0 .../ModelConfiguration/DeviceConfiguration.cs | 0 .../DeviceOptionsConfiguration.cs | 0 .../DisplayPreferencesConfiguration.cs | 0 .../ItemValuesConfiguration.cs | 0 .../ItemValuesMapConfiguration.cs | 0 .../MediaStreamInfoConfiguration.cs | 0 .../PeopleBaseItemMapConfiguration.cs | 0 .../ModelConfiguration/PeopleConfiguration.cs | 0 .../PermissionConfiguration.cs | 0 .../PreferenceConfiguration.cs | 0 .../TrickplayInfoConfiguration.cs | 0 .../ModelConfiguration/UserConfiguration.cs | 0 .../UserDataConfiguration.cs | 0 .../Jellyfin.Database.Providers.SqLite.csproj | 34 +++++------- .../Migrations/.gitattributes | 0 .../20200514181226_AddActivityLog.Designer.cs | 0 .../20200514181226_AddActivityLog.cs | 0 .../20200613202153_AddUsers.Designer.cs | 0 .../Migrations/20200613202153_AddUsers.cs | 0 ...28005145_AddDisplayPreferences.Designer.cs | 0 .../20200728005145_AddDisplayPreferences.cs | 0 ...533_FixDisplayPreferencesIndex.Designer.cs | 0 ...200905220533_FixDisplayPreferencesIndex.cs | 0 ...004171403_AddMaxActiveSessions.Designer.cs | 0 .../20201004171403_AddMaxActiveSessions.cs | 0 ...55_AddCustomDisplayPreferences.Designer.cs | 0 ...01204223655_AddCustomDisplayPreferences.cs | 0 ...181425_AddIndexesAndCollations.Designer.cs | 0 .../20210320181425_AddIndexesAndCollations.cs | 0 ...110544_NullableCustomPrefValue.Designer.cs | 0 .../20210407110544_NullableCustomPrefValue.cs | 0 .../20210814002109_AddDevices.Designer.cs | 0 .../Migrations/20210814002109_AddDevices.cs | 0 ...ddIndexActivityLogsDateCreated.Designer.cs | 0 ...2080052_AddIndexActivityLogsDateCreated.cs | 0 ...30526173516_RemoveEasyPassword.Designer.cs | 0 .../20230526173516_RemoveEasyPassword.cs | 0 ...230626233818_AddTrickplayInfos.Designer.cs | 0 .../20230626233818_AddTrickplayInfos.cs | 0 ...0230923170422_UserCastReceiver.Designer.cs | 0 .../20230923170422_UserCastReceiver.cs | 0 ...0240729140605_AddMediaSegments.Designer.cs | 0 .../20240729140605_AddMediaSegments.cs | 0 ...rkSegmentProviderIdNonNullable.Designer.cs | 0 ...082930_MarkSegmentProviderIdNonNullable.cs | 0 ...41020103111_LibraryDbMigration.Designer.cs | 0 .../20241020103111_LibraryDbMigration.cs | 0 ...41111131257_AddedCustomDataKey.Designer.cs | 0 .../20241111131257_AddedCustomDataKey.cs | 0 ...11135439_AddedCustomDataKeyKey.Designer.cs | 0 .../20241111135439_AddedCustomDataKeyKey.cs | 0 ...1112152323_FixAncestorIdConfig.Designer.cs | 0 .../20241112152323_FixAncestorIdConfig.cs | 0 ...20241112232041_fixMediaStreams.Designer.cs | 0 .../20241112232041_fixMediaStreams.cs | 0 ...0241112234144_FixMediaStreams2.Designer.cs | 0 .../20241112234144_FixMediaStreams2.cs | 0 ...3133548_EnforceUniqueItemValue.Designer.cs | 0 .../20241113133548_EnforceUniqueItemValue.cs | 0 .../20250202021306_FixedCollation.Designer.cs | 0 .../20250202021306_FixedCollation.cs | 0 ...92455_MakeStartEndDateNullable.Designer.cs | 0 ...20250204092455_MakeStartEndDateNullable.cs | 0 .../20250214031148_ChannelIdGuid.Designer.cs | 0 .../20250214031148_ChannelIdGuid.cs | 0 .../Migrations/JellyfinDbModelSnapshot.cs | 0 .../SqliteDesignTimeJellyfinDbFactory.cs | 0 .../ModelBuilderExtensions.cs | 0 .../Properties/AssemblyInfo.cs | 0 .../SqliteDatabaseProvider.cs | 3 + .../DateTimeKindValueConverter.cs | 0 .../Jellyfin.Database}/readme.md | 0 188 files changed, 49 insertions(+), 231 deletions(-) delete mode 100644 .devcontainer/pgsql/Dockerfile delete mode 100644 .devcontainer/pgsql/devcontainer.json delete mode 100644 .devcontainer/pgsql/docker-compose.yaml delete mode 100644 Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/PostgreSqlOptions.cs delete mode 100644 Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs (69%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/ActivityLog.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/AncestorId.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Chapter.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Group.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/HomeSection.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/ImageInfo.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/ItemValue.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/ItemValueType.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/MediaSegment.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/People.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Permission.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Preference.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Security/Device.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/User.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Entities/UserData.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/ArtKind.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/IndexingKind.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/PermissionKind.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/SortOrder.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Enums/ViewType.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs (100%) create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/JellyfinDbContext.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs (100%) rename Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj => src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj (50%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/.gitattributes (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.Designer.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/ModelBuilderExtensions.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/Properties/AssemblyInfo.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs (97%) rename {Jellyfin.Database => src/Jellyfin.Database}/Jellyfin.Database.Providers.SqLite/ValueConverters/DateTimeKindValueConverter.cs (100%) rename {Jellyfin.Database => src/Jellyfin.Database}/readme.md (100%) diff --git a/.devcontainer/pgsql/Dockerfile b/.devcontainer/pgsql/Dockerfile deleted file mode 100644 index ff7f3bcd79..0000000000 --- a/.devcontainer/pgsql/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm - -# [Optional] Uncomment this section to install additional OS packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends - -# [Optional] Uncomment this line to install global node packages. -# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/pgsql/devcontainer.json b/.devcontainer/pgsql/devcontainer.json deleted file mode 100644 index 3dd91d9755..0000000000 --- a/.devcontainer/pgsql/devcontainer.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "Development Jellyfin Server", - "dockerComposeFile": "docker-compose.yaml", - "service": "app", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - // restores nuget packages, installs the dotnet workloads and installs the dev https certificate - "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"", - // reads the extensions list and installs them - "postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension", - "forwardPorts": [ - "pgadmin:8081" - ], - "portsAttributes": { - "8081": { - "label": "pgAdmin", - "onAutoForward": "notify", - "requireLocalPort": true - }, - "8096": { - "label": "jellyfinapi", - "onAutoForward": "notify", - "requireLocalPort": true - } - }, - "features": { - "ghcr.io/devcontainers/features/dotnet:2": { - "version": "none", - "dotnetRuntimeVersions": "9.0", - "aspNetCoreRuntimeVersions": "9.0" - }, - "ghcr.io/devcontainers-contrib/features/apt-packages:1": { - "preserve_apt_list": false, - "packages": [ - "libfontconfig1" - ] - }, - "ghcr.io/devcontainers/features/docker-in-docker:2": { - "dockerDashComposeVersion": "v2" - }, - "ghcr.io/devcontainers/features/github-cli:1": {}, - "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {} - }, - "hostRequirements": { - "memory": "8gb", - "cpus": 4 - } -} diff --git a/.devcontainer/pgsql/docker-compose.yaml b/.devcontainer/pgsql/docker-compose.yaml deleted file mode 100644 index 45af0b33e1..0000000000 --- a/.devcontainer/pgsql/docker-compose.yaml +++ /dev/null @@ -1,48 +0,0 @@ -version: '3.8' - -services: - app: - build: - context: . - dockerfile: Dockerfile - - volumes: - - ../../..:/workspaces:cached - - # Overrides default command so things don't shut down after the process ends. - command: sleep infinity - - # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. - network_mode: service:pgadmin - # Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - # user: root - - # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. - # (Adding the "ports" property to this file will not forward from a Codespace.) - - db: - image: postgres:17.2 - restart: unless-stopped - volumes: - - ./pgdata:/var/lib/postgresql/data - environment: - POSTGRES_PASSWORD: jellyfin - POSTGRES_USER: jellyfin - POSTGRES_DB: Jellyfin - pgadmin: - image: dpage/pgadmin4 - restart: unless-stopped - volumes: - - ./pgadmin:/pgadmin - - pgadmin-data:/var/lib/pgadmin - environment: - - PGADMIN_DEFAULT_EMAIL=user@domain.com - - PGADMIN_DEFAULT_PASSWORD=SuperSecret - - PGADMIN_LISTEN_PORT=8081 - - PGADMIN_SERVER_JSON_FILE=/pgadmin/servers.json - - PGADMIN_CONFIG_SERVER_MODE=False - # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally. - # (Adding the "ports" property to this file will not forward from a Codespace.) - -volumes: - pgadmin-data: diff --git a/.gitignore b/.gitignore index fe312b647c..d5a0367bff 100644 --- a/.gitignore +++ b/.gitignore @@ -277,5 +277,3 @@ apiclient/generated # Omnisharp crash logs mono_crash.*.json - -.devcontainer/pgsql/pgdata \ No newline at end of file diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 432f1846e5..45374c22f7 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -39,7 +39,7 @@ - + diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/PostgreSqlOptions.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/PostgreSqlOptions.cs deleted file mode 100644 index 1f7c30b098..0000000000 --- a/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/PostgreSqlOptions.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; - -namespace Jellyfin.Server.Implementations.DatabaseConfiguration; - -/// -/// Options specific to run jellyfin on a postgreSql database. -/// -public class PostgreSqlOptions -{ - /// - /// Gets or Sets the Port. Defaults to 5432. - /// - public required int Port { get; set; } = 5432; - - /// - /// Gets or Sets the Server name. - /// - public required string ServerName { get; set; } - - /// - /// Gets or Sets the username. - /// - public required string Username { get; set; } - - /// - /// Gets or Sets the password. - /// - public required string Password { get; set; } - - /// - /// Gets or Sets the database name. Defaults to "Jellyfin". - /// - public string DatabaseName { get; set; } = "Jellyfin"; - - /// - /// Gets or Sets the timeout in secounds before a running command is terminated. Defaults to 30. - /// - public int Timeout { get; set; } = 30; -} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj deleted file mode 100644 index fca872d902..0000000000 --- a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj +++ /dev/null @@ -1,55 +0,0 @@ - - - - net9.0 - enable - enable - false - true - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 01d9dcf64d..e73f87d39e 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -35,9 +35,8 @@ - - - + + diff --git a/Jellyfin.sln b/Jellyfin.sln index 5bc1b6ef8a..78f7cce09e 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -87,11 +87,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.LiveTv.Tests", "te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.LiveTv", "src\Jellyfin.LiveTv\Jellyfin.LiveTv.csproj", "{8C6B2B13-58A4-4506-9DAB-1F882A093FE0}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jellyfin.Database", "Jellyfin.Database", "{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jellyfin.Database", "src\Jellyfin.Database", "{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Providers.SqLite", "Jellyfin.Database\Jellyfin.Database.Providers.SqLite\Jellyfin.Database.Providers.SqLite.csproj", "{A5590358-33CC-4B39-BDE7-DC62FEB03C76}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Providers.SqLite", "src\Jellyfin.Database\Jellyfin.Database.Providers.SqLite\Jellyfin.Database.Providers.SqLite.csproj", "{A5590358-33CC-4B39-BDE7-DC62FEB03C76}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Implementations", "Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj", "{8C9F9221-8415-496C-B1F5-E7756F03FA59}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Implementations", "src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj", "{8C9F9221-8415-496C-B1F5-E7756F03FA59}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -283,6 +283,7 @@ Global {24960660-DE6C-47BF-AEEF-CEE8F19FE6C2} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {8C6B2B13-58A4-4506-9DAB-1F882A093FE0} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} + {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} {A5590358-33CC-4B39-BDE7-DC62FEB03C76} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} {8C9F9221-8415-496C-B1F5-E7756F03FA59} = {4C54CE05-69C8-48FA-8785-39F7F6DB1CAD} EndGlobalSection diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs similarity index 69% rename from Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs index d49d8536a3..af2ede7010 100644 --- a/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs @@ -11,9 +11,4 @@ public class DatabaseConfigurationOptions /// Gets or Sets the type of database jellyfin should use. /// public required string DatabaseType { get; set; } - - /// - /// Gets or Sets the settings to run jellyfin with Postgres. - /// - public PostgreSqlOptions? PostgreSql { get; set; } } diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj new file mode 100644 index 0000000000..3b619cce61 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + false + true + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj similarity index 50% rename from Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj index 96cea69dfc..4e5f63f6fe 100644 --- a/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj @@ -2,33 +2,16 @@ net9.0 - enable - enable false true - - - - - all - runtime; build; native; contentfiles; analyzers - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - + + + all @@ -40,4 +23,15 @@ + + + + + + + + + + + diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/.gitattributes b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/.gitattributes similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/.gitattributes rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/.gitattributes diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.Designer.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.Designer.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ModelBuilderExtensions.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ModelBuilderExtensions.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ModelBuilderExtensions.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ModelBuilderExtensions.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Properties/AssemblyInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Properties/AssemblyInfo.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Properties/AssemblyInfo.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Properties/AssemblyInfo.cs diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs similarity index 97% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs index ef4f00384b..f7fde4989e 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs @@ -1,4 +1,7 @@ using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; using Jellyfin.Server.Implementations; using MediaBrowser.Common.Configuration; using Microsoft.Data.Sqlite; diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ValueConverters/DateTimeKindValueConverter.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ValueConverters/DateTimeKindValueConverter.cs similarity index 100% rename from Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ValueConverters/DateTimeKindValueConverter.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ValueConverters/DateTimeKindValueConverter.cs diff --git a/Jellyfin.Database/readme.md b/src/Jellyfin.Database/readme.md similarity index 100% rename from Jellyfin.Database/readme.md rename to src/Jellyfin.Database/readme.md From 69e3e4c468c87d5802e470977ade40340297f3e0 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 20 Feb 2025 09:59:21 +0000 Subject: [PATCH 038/508] Fixed readme for migrations --- src/Jellyfin.Database/readme.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Jellyfin.Database/readme.md b/src/Jellyfin.Database/readme.md index 55d57d3eff..a0f8cefc4a 100644 --- a/src/Jellyfin.Database/readme.md +++ b/src/Jellyfin.Database/readme.md @@ -2,7 +2,7 @@ This shall provide context on how to work with entity frameworks multi provider migration feature. -Jellyfin supports multiple database providers, namely SqLite as its default and the experimental postgresSQL. +Jellyfin will in the future support multiple database providers, namely SqLite as its default and the experimental postgresSQL. Each provider has its own set of migrations, as they contain provider specific instructions to migrate the specific changes to their respective systems. @@ -12,13 +12,12 @@ When creating a new migration, you always have to create migrations for all prov dotnet ef migrations add MIGRATION_NAME --project "PATH_TO_PROJECT" -- --provider PROVIDER_KEY ``` -with both sqlite and pgsql currently beeing supported and both need migrations, you need to run the Entity Framework tool with the correct project to tell EFCore where to store the migrations and the correct provider key to tell jellyfin to load that provider. +with sqlite currently beeing supported and, you need to run the Entity Framework tool with the correct project to tell EFCore where to store the migrations and the correct provider key to tell jellyfin to load that provider. The example is made from the root folder of the project e.g for codespaces `/workspaces/jellyfin` ```cmd dotnet ef migrations add {MIGRATION_NAME} --project "Jellyfin.Database/Jellyfin.Database.Providers.SqLite" -- --migration-provider Jellyfin-SQLite -dotnet ef migrations add {MIGRATION_NAME} --project "Jellyfin.Database/Jellyfin.Database.Providers.PgSql" -- --migration-provider Jellyfin-PgSql ``` If you get the error: `Run "dotnet tool restore" to make the "dotnet-ef" command available.` Run `dotnet restore`. From 3e223ead1e44559f3bd06ad4d00b43c57c86b6b5 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 20 Feb 2025 10:02:47 +0000 Subject: [PATCH 039/508] Fixed references for database projects --- Jellyfin.Server/Jellyfin.Server.csproj | 2 +- .../Jellyfin.Server.Implementations.Tests.csproj | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index bd094d6914..452b03efbe 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -66,7 +66,7 @@ - + diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 84a3951f67..b54e2baf6a 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -29,9 +29,8 @@ - - - + + From 05f5d19ff45efe1a30a2e33e9aa2366788e8abb9 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 20 Feb 2025 19:56:59 +0000 Subject: [PATCH 040/508] fixed new project paths --- .../Emby.Server.Implementations.csproj | 2 +- src/Jellyfin.Database/readme.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index c94ff924c5..8f89f35ac9 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/Jellyfin.Database/readme.md b/src/Jellyfin.Database/readme.md index a0f8cefc4a..dc1479897d 100644 --- a/src/Jellyfin.Database/readme.md +++ b/src/Jellyfin.Database/readme.md @@ -12,14 +12,14 @@ When creating a new migration, you always have to create migrations for all prov dotnet ef migrations add MIGRATION_NAME --project "PATH_TO_PROJECT" -- --provider PROVIDER_KEY ``` -with sqlite currently beeing supported and, you need to run the Entity Framework tool with the correct project to tell EFCore where to store the migrations and the correct provider key to tell jellyfin to load that provider. +with sqlite currently being the only supported provider, you need to run the Entity Framework tool with the correct project to tell EFCore where to store the migrations and the correct provider key to tell jellyfin to load that provider. The example is made from the root folder of the project e.g for codespaces `/workspaces/jellyfin` ```cmd -dotnet ef migrations add {MIGRATION_NAME} --project "Jellyfin.Database/Jellyfin.Database.Providers.SqLite" -- --migration-provider Jellyfin-SQLite +dotnet ef migrations add {MIGRATION_NAME} --project "src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite" -- --migration-provider Jellyfin-SQLite ``` If you get the error: `Run "dotnet tool restore" to make the "dotnet-ef" command available.` Run `dotnet restore`. -in the event that you get the error: `System.UnauthorizedAccessException: Access to the path '/Jellyfin.Database' is denied.` you have to restore as sudo and then run `ef migrations` as sudo too. +in the event that you get the error: `System.UnauthorizedAccessException: Access to the path '/src/Jellyfin.Database' is denied.` you have to restore as sudo and then run `ef migrations` as sudo too. From 5ff2767012e2970cedb697a48eabd4949c348f2c Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Fri, 21 Feb 2025 11:58:46 +0100 Subject: [PATCH 041/508] Use TryGetProviderId where possible --- .../Entities/Audio/MusicArtist.cs | 6 +-- .../Movies/ImdbExternalUrlProvider.cs | 4 +- .../AudioDbAlbumExternalUrlProvider.cs | 3 +- .../AudioDb/AudioDbAlbumImageProvider.cs | 7 +-- .../AudioDbArtistExternalUrlProvider.cs | 3 +- .../AudioDb/AudioDbArtistImageProvider.cs | 13 +++--- ...sicBrainzAlbumArtistExternalUrlProvider.cs | 3 +- .../MusicBrainzAlbumExternalUrlProvider.cs | 3 +- .../MusicBrainzArtistExternalUrlProvider.cs | 3 +- ...icBrainzReleaseGroupExternalUrlProvider.cs | 3 +- .../MusicBrainzTrackExternalUrlProvider.cs | 3 +- .../Plugins/Tmdb/TmdbExternalUrlProvider.cs | 18 +++----- .../TV/Zap2ItExternalUrlProvider.cs | 3 +- .../Savers/BaseNfoSaver.cs | 44 +++++-------------- .../Savers/MovieNfoSaver.cs | 4 +- .../Savers/SeriesNfoSaver.cs | 4 +- .../Recordings/RecordingsMetadataManager.cs | 13 ++---- 17 files changed, 42 insertions(+), 95 deletions(-) diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index ecb3ac3a68..52221ad9e3 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -138,11 +138,9 @@ namespace MediaBrowser.Controller.Entities.Audio private static List GetUserDataKeys(MusicArtist item) { var list = new List(); - var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist); - - if (!string.IsNullOrEmpty(id)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var externalId)) { - list.Add("Artist-Musicbrainz-" + id); + list.Add("Artist-Musicbrainz-" + externalId); } list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics()); diff --git a/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs index eadcc976af..ff8ad1d612 100644 --- a/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs @@ -17,9 +17,7 @@ public class ImdbExternalUrlProvider : IExternalUrlProvider public IEnumerable GetExternalUrls(BaseItem item) { var baseUrl = "https://www.imdb.com/"; - var externalId = item.GetProviderId(MetadataProvider.Imdb); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Imdb, out var externalId)) { yield return baseUrl + $"title/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs index 1615f1ce59..01d2841059 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs @@ -17,8 +17,7 @@ public class AudioDbAlbumExternalUrlProvider : IExternalUrlProvider /// public IEnumerable GetExternalUrls(BaseItem item) { - var externalId = item.GetProviderId(MetadataProvider.AudioDbAlbum); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.AudioDbAlbum, out var externalId)) { var baseUrl = "https://www.theaudiodb.com/"; switch (item) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs index 8a516e1ce7..d2eeb7f079 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading; @@ -50,9 +49,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) { - var id = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); - - if (!string.IsNullOrWhiteSpace(id)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var id)) { await AudioDbAlbumProvider.Current.EnsureInfo(id, cancellationToken).ConfigureAwait(false); @@ -70,7 +67,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb } } - return Enumerable.Empty(); + return []; } private List GetImages(AudioDbAlbumProvider.Album item) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs index 5c5057fa1a..56b0d9bcb2 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs @@ -17,8 +17,7 @@ public class AudioDbArtistExternalUrlProvider : IExternalUrlProvider /// public IEnumerable GetExternalUrls(BaseItem item) { - var externalId = item.GetProviderId(MetadataProvider.AudioDbArtist); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.AudioDbArtist, out var externalId)) { var baseUrl = "https://www.theaudiodb.com/"; switch (item) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs index 4e7757cd26..88730f34d2 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading; @@ -43,21 +42,19 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public IEnumerable GetSupportedImages(BaseItem item) { - return new ImageType[] - { + return + [ ImageType.Primary, ImageType.Logo, ImageType.Banner, ImageType.Backdrop - }; + ]; } /// public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) { - var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist); - - if (!string.IsNullOrWhiteSpace(id)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var id)) { await AudioDbArtistProvider.Current.EnsureArtistInfo(id, cancellationToken).ConfigureAwait(false); @@ -75,7 +72,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb } } - return Enumerable.Empty(); + return []; } private List GetImages(AudioDbArtistProvider.Artist item) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs index 3de18f4ccf..29dbbc58c1 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs @@ -19,8 +19,7 @@ public class MusicBrainzAlbumArtistExternalUrlProvider : IExternalUrlProvider { if (item is MusicAlbum) { - var externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs index 6d0afdd508..f838dcf4c8 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs @@ -19,8 +19,7 @@ public class MusicBrainzAlbumExternalUrlProvider : IExternalUrlProvider { if (item is MusicAlbum) { - var externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/release/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs index cd71191bff..ee5a597c62 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs @@ -17,8 +17,7 @@ public class MusicBrainzArtistExternalUrlProvider : IExternalUrlProvider /// public IEnumerable GetExternalUrls(BaseItem item) { - var externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var externalId)) { switch (item) { diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs index 9bc0103794..dd0a939f72 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs @@ -19,8 +19,7 @@ public class MusicBrainzReleaseGroupExternalUrlProvider : IExternalUrlProvider { if (item is MusicAlbum) { - var externalId = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/release-group/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs index fc26dc54df..59e6f42b19 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs @@ -19,8 +19,7 @@ public class MusicBrainzTrackExternalUrlProvider : IExternalUrlProvider { if (item is Audio) { - var externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/track/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs index b8fd18f286..bec800c035 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs @@ -23,16 +23,14 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider switch (item) { case Series: - var externalId = item.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out var externalId)) { yield return TmdbUtils.BaseTmdbUrl + $"tv/{externalId}"; } break; case Season season: - var seriesExternalId = season.Series.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(seriesExternalId)) + if (season.Series.TryGetProviderId(MetadataProvider.Tmdb, out var seriesExternalId)) { var orderString = season.Series.DisplayOrder; if (string.IsNullOrEmpty(orderString)) @@ -52,8 +50,7 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider break; case Episode episode: - seriesExternalId = episode.Series.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(seriesExternalId)) + if (episode.Series.TryGetProviderId(MetadataProvider.Imdb, out seriesExternalId)) { var orderString = episode.Series.DisplayOrder; if (string.IsNullOrEmpty(orderString)) @@ -73,24 +70,21 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider break; case Movie: - externalId = item.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out externalId)) { yield return TmdbUtils.BaseTmdbUrl + $"movie/{externalId}"; } break; case Person: - externalId = item.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out externalId)) { yield return TmdbUtils.BaseTmdbUrl + $"person/{externalId}"; } break; case BoxSet: - externalId = item.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out externalId)) { yield return TmdbUtils.BaseTmdbUrl + $"collection/{externalId}"; } diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs index f6516fddeb..52b0583e58 100644 --- a/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs +++ b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs @@ -16,8 +16,7 @@ public class Zap2ItExternalUrlProvider : IExternalUrlProvider /// public IEnumerable GetExternalUrls(BaseItem item) { - var externalId = item.GetProviderId(MetadataProvider.Zap2It); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Zap2It, out var externalId)) { yield return $"http://tvlistings.zap2it.com/overview.html?programSeriesId={externalId}"; } diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 51c5a20803..0786e7afde 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -544,16 +544,13 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio); } - var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection); - - if (!string.IsNullOrEmpty(tmdbCollection)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbCollection)) { writer.WriteElementString("collectionnumber", tmdbCollection); writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString()); } - var imdb = item.GetProviderId(MetadataProvider.Imdb); - if (!string.IsNullOrEmpty(imdb)) + if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb)) { if (item is Series) { @@ -570,16 +567,14 @@ namespace MediaBrowser.XbmcMetadata.Savers // Series xml saver already saves this if (item is not Series) { - var tvdb = item.GetProviderId(MetadataProvider.Tvdb); - if (!string.IsNullOrEmpty(tvdb)) + if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb)) { writer.WriteElementString("tvdbid", tvdb); writtenProviderIds.Add(MetadataProvider.Tvdb.ToString()); } } - var tmdb = item.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(tmdb)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdb)) { writer.WriteElementString("tmdbid", tmdb); writtenProviderIds.Add(MetadataProvider.Tmdb.ToString()); @@ -687,64 +682,49 @@ namespace MediaBrowser.XbmcMetadata.Savers } } - var externalId = item.GetProviderId(MetadataProvider.AudioDbArtist); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.AudioDbArtist, out var externalId)) { writer.WriteElementString("audiodbartistid", externalId); writtenProviderIds.Add(MetadataProvider.AudioDbArtist.ToString()); } - externalId = item.GetProviderId(MetadataProvider.AudioDbAlbum); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.AudioDbAlbum, out externalId)) { writer.WriteElementString("audiodbalbumid", externalId); writtenProviderIds.Add(MetadataProvider.AudioDbAlbum.ToString()); } - externalId = item.GetProviderId(MetadataProvider.Zap2It); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Zap2It, out externalId)) { writer.WriteElementString("zap2itid", externalId); writtenProviderIds.Add(MetadataProvider.Zap2It.ToString()); } - externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbum); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out externalId)) { writer.WriteElementString("musicbrainzalbumid", externalId); writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbum.ToString()); } - externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out externalId)) { writer.WriteElementString("musicbrainzalbumartistid", externalId); writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbumArtist.ToString()); } - externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out externalId)) { writer.WriteElementString("musicbrainzartistid", externalId); writtenProviderIds.Add(MetadataProvider.MusicBrainzArtist.ToString()); } - externalId = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out externalId)) { writer.WriteElementString("musicbrainzreleasegroupid", externalId); writtenProviderIds.Add(MetadataProvider.MusicBrainzReleaseGroup.ToString()); } - externalId = item.GetProviderId(MetadataProvider.TvRage); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.TvRage, out externalId)) { writer.WriteElementString("tvrageid", externalId); writtenProviderIds.Add(MetadataProvider.TvRage.ToString()); diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index e85e369d91..a5909762d6 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -91,9 +91,7 @@ namespace MediaBrowser.XbmcMetadata.Savers /// protected override void WriteCustomElements(BaseItem item, XmlWriter writer) { - var imdb = item.GetProviderId(MetadataProvider.Imdb); - - if (!string.IsNullOrEmpty(imdb)) + if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb)) { writer.WriteElementString("id", imdb); } diff --git a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs index 083f22e5d2..1ac6768a16 100644 --- a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs @@ -54,9 +54,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { var series = (Series)item; - var tvdb = item.GetProviderId(MetadataProvider.Tvdb); - - if (!string.IsNullOrEmpty(tvdb)) + if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb)) { writer.WriteElementString("id", tvdb); diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs index b2b82332df..3a2c463695 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs @@ -344,15 +344,12 @@ public class RecordingsMetadataManager await writer.WriteElementStringAsync(null, "credits", null, person).ConfigureAwait(false); } - var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection); - - if (!string.IsNullOrEmpty(tmdbCollection)) + if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection)) { await writer.WriteElementStringAsync(null, "collectionnumber", null, tmdbCollection).ConfigureAwait(false); } - var imdb = item.GetProviderId(MetadataProvider.Imdb); - if (!string.IsNullOrEmpty(imdb)) + if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb)) { if (!isSeriesEpisode) { @@ -365,8 +362,7 @@ public class RecordingsMetadataManager lockData = false; } - var tvdb = item.GetProviderId(MetadataProvider.Tvdb); - if (!string.IsNullOrEmpty(tvdb)) + if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb)) { await writer.WriteElementStringAsync(null, "tvdbid", null, tvdb).ConfigureAwait(false); @@ -374,8 +370,7 @@ public class RecordingsMetadataManager lockData = false; } - var tmdb = item.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(tmdb)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdb)) { await writer.WriteElementStringAsync(null, "tmdbid", null, tmdb).ConfigureAwait(false); From a05b3be1b3234102e4225aed57f24a598fd8edf1 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 21 Feb 2025 11:00:01 +0000 Subject: [PATCH 042/508] Fixed nullability on startupService --- Jellyfin.Server/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 922a06802a..8523639e77 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -45,7 +45,7 @@ namespace Jellyfin.Server public const string LoggingConfigFileSystem = "logging.json"; private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); - private static SetupServer? _setupServer = new(); + private static SetupServer _setupServer = new(); private static CoreAppHost? _appHost; private static IHost? _jfHost = null; private static long _startTimestamp; @@ -74,7 +74,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); - await _setupServer!.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); + await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -169,7 +169,7 @@ namespace Jellyfin.Server try { - await _setupServer!.StopAsync().ConfigureAwait(false); + await _setupServer.StopAsync().ConfigureAwait(false); _setupServer.Dispose(); _setupServer = null!; await _jfHost.StartAsync().ConfigureAwait(false); From 7735aafef5908f2d15f6dc2b3da3bb3795fd83d2 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 21 Feb 2025 11:05:47 +0000 Subject: [PATCH 043/508] renaming of jfHost usings cleanup --- Jellyfin.Server/Program.cs | 17 ++++++++--------- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 3 --- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 8523639e77..3d92caac46 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -7,7 +7,6 @@ using System.Reflection; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; -using Jellyfin.Networking.Manager; using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; using Jellyfin.Server.Implementations; @@ -47,7 +46,7 @@ namespace Jellyfin.Server private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static SetupServer _setupServer = new(); private static CoreAppHost? _appHost; - private static IHost? _jfHost = null; + private static IHost? _jellyfinHost = null; private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; @@ -74,7 +73,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); - await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); + await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -130,7 +129,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); _setupServer = new SetupServer(); - await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); + await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); } } while (_restartOnShutdown); } @@ -145,7 +144,7 @@ namespace Jellyfin.Server _appHost = appHost; try { - _jfHost = Host.CreateDefaultBuilder() + _jellyfinHost = Host.CreateDefaultBuilder() .UseConsoleLifetime() .ConfigureServices(services => appHost.Init(services)) .ConfigureWebHostDefaults(webHostBuilder => @@ -162,7 +161,7 @@ namespace Jellyfin.Server .Build(); // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. - appHost.ServiceProvider = _jfHost.Services; + appHost.ServiceProvider = _jellyfinHost.Services; await appHost.InitializeServices().ConfigureAwait(false); Migrations.MigrationRunner.Run(appHost, _loggerFactory); @@ -172,7 +171,7 @@ namespace Jellyfin.Server await _setupServer.StopAsync().ConfigureAwait(false); _setupServer.Dispose(); _setupServer = null!; - await _jfHost.StartAsync().ConfigureAwait(false); + await _jellyfinHost.StartAsync().ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) { @@ -191,7 +190,7 @@ namespace Jellyfin.Server _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); - await _jfHost.WaitForShutdownAsync().ConfigureAwait(false); + await _jellyfinHost.WaitForShutdownAsync().ConfigureAwait(false); _restartOnShutdown = appHost.ShouldRestart; } catch (Exception ex) @@ -217,7 +216,7 @@ namespace Jellyfin.Server } _appHost = null; - _jfHost?.Dispose(); + _jellyfinHost?.Dispose(); } } diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index ea4804753b..09b7434eff 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -4,19 +4,16 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Model.System; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; -using SQLitePCL; namespace Jellyfin.Server.ServerSetupApp; From 963f2357a966dd7a5a6ab248155cc52ce066753b Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 21 Feb 2025 11:06:28 +0000 Subject: [PATCH 044/508] simplified logfile path --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 09b7434eff..9e2cf5bc8b 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -64,10 +64,14 @@ public sealed class SetupServer : IDisposable return; } - var logfilePath = Directory.EnumerateFiles(applicationPaths.LogDirectoryPath).Select(e => new FileInfo(e)).OrderBy(f => f.CreationTimeUtc).FirstOrDefault()?.FullName; - if (logfilePath is not null) + var logFilePath = new DirectoryInfo(applicationPaths.LogDirectoryPath) + .EnumerateFiles() + .OrderBy(f => f.CreationTimeUtc) + .FirstOrDefault() + ?.FullName; + if (logFilePath is not null) { - await context.Response.SendFileAsync(logfilePath, CancellationToken.None).ConfigureAwait(false); + await context.Response.SendFileAsync(logFilePath, CancellationToken.None).ConfigureAwait(false); } }); }); From a4aefc8a802d89c2f1406261d492bfc298351bb7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 20:25:39 +0000 Subject: [PATCH 045/508] Update CI dependencies --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/ci-openapi.yml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 00f7e9e6d2..1f166d10c8 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.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 diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 07e61024ee..1975a9f03a 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -26,7 +26,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@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 diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index ee9df30f44..bc30256568 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.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 From 83b2c472378410a3d0e7e8065cb627d2520d99d4 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sat, 22 Feb 2025 10:23:33 +0100 Subject: [PATCH 046/508] Remove deprecated GetWakeOnLanInfo endpoint --- Jellyfin.Api/Controllers/SystemController.cs | 16 ------- MediaBrowser.Model/System/WakeOnLanInfo.cs | 47 -------------------- 2 files changed, 63 deletions(-) delete mode 100644 MediaBrowser.Model/System/WakeOnLanInfo.cs diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 6c5ce47158..0ee11c0704 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/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"); } - - /// - /// Gets wake on lan information. - /// - /// Information retrieved. - /// An with the WakeOnLan infos. - [HttpGet("WakeOnLanInfo")] - [Authorize] - [Obsolete("This endpoint is obsolete.")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetWakeOnLanInfo() - { - var result = _networkManager.GetMacAddresses() - .Select(i => new WakeOnLanInfo(i)); - return Ok(result); - } } diff --git a/MediaBrowser.Model/System/WakeOnLanInfo.cs b/MediaBrowser.Model/System/WakeOnLanInfo.cs deleted file mode 100644 index aba19a6baf..0000000000 --- a/MediaBrowser.Model/System/WakeOnLanInfo.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Net.NetworkInformation; - -namespace MediaBrowser.Model.System -{ - /// - /// Provides the MAC address and port for wake-on-LAN functionality. - /// - public class WakeOnLanInfo - { - /// - /// Initializes a new instance of the class. - /// - /// The MAC address. - public WakeOnLanInfo(PhysicalAddress macAddress) : this(macAddress.ToString()) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The MAC address. - public WakeOnLanInfo(string macAddress) : this() - { - MacAddress = macAddress; - } - - /// - /// Initializes a new instance of the class. - /// - public WakeOnLanInfo() - { - Port = 9; - } - - /// - /// Gets the MAC address of the device. - /// - /// The MAC address. - public string? MacAddress { get; } - - /// - /// Gets or sets the wake-on-LAN port. - /// - /// The wake-on-LAN port. - public int Port { get; set; } - } -} From d18066f0f23f819efdef145a74ffb173df636b58 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sat, 22 Feb 2025 10:27:42 +0100 Subject: [PATCH 047/508] Remove GetMacAddresses from NetworkManager --- MediaBrowser.Common/Net/INetworkManager.cs | 6 ----- .../Manager/NetworkManager.cs | 23 ------------------- 2 files changed, 29 deletions(-) diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 78a391d36d..d838144ff6 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -94,12 +94,6 @@ namespace MediaBrowser.Common.Net /// IP address to use, or loopback address if all else fails. string GetBindAddress(string source, out int? port); - /// - /// Get a list of all the MAC addresses associated with active interfaces. - /// - /// List of MAC addresses. - IReadOnlyList GetMacAddresses(); - /// /// Returns true if the address is part of the user defined LAN. /// diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index dd01e9533b..2fbcbf79ce 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -49,11 +49,6 @@ public class NetworkManager : INetworkManager, IDisposable /// private bool _eventfire; - /// - /// List of all interface MAC addresses. - /// - private IReadOnlyList _macAddresses; - /// /// Dictionary containing interface addresses and their subnets. /// @@ -91,7 +86,6 @@ public class NetworkManager : INetworkManager, IDisposable _startupConfig = startupConfig; _initLock = new(); _interfaces = new List(); - _macAddresses = new List(); _publishedServerUrls = new List(); _networkEventLock = new(); _remoteAddressFilter = new List(); @@ -215,7 +209,6 @@ public class NetworkManager : INetworkManager, IDisposable /// /// 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. /// private void InitializeInterfaces() { @@ -224,7 +217,6 @@ public class NetworkManager : INetworkManager, IDisposable _logger.LogDebug("Refreshing interfaces."); var interfaces = new List(); - var macAddresses = new List(); 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; } - /// - public IReadOnlyList GetMacAddresses() - { - // Populated in construction - so always has values. - return _macAddresses; - } - /// public IReadOnlyList GetLoopbacks() { From 260f1323d8bf73d4fc671991ed743d90cfe4aade Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 22 Feb 2025 18:59:37 +0100 Subject: [PATCH 048/508] Apply suggestions from code review Co-authored-by: Cody Robibero --- .../MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs | 2 +- .../Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs index 29dbbc58c1..f4b3f4f8c2 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs @@ -19,7 +19,7 @@ public class MusicBrainzAlbumArtistExternalUrlProvider : IExternalUrlProvider { if (item is MusicAlbum) { - if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out var externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs index f838dcf4c8..b9d3b48353 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs @@ -19,7 +19,7 @@ public class MusicBrainzAlbumExternalUrlProvider : IExternalUrlProvider { if (item is MusicAlbum) { - if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out var externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/release/{externalId}"; } From 7f8eb179a615449c3ef87b1952af1899c904024a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 23 Feb 2025 12:38:16 +0000 Subject: [PATCH 049/508] Update dependency z440.atl.core to 6.18.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 854c5a6df8..d23d704d5b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -79,7 +79,7 @@ - + From 068bc687647ed894a2ae05d5b1e24770415bc8ec Mon Sep 17 00:00:00 2001 From: millallo Date: Sat, 22 Feb 2025 12:31:20 +0000 Subject: [PATCH 050/508] Translated using Weblate (Italian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/ --- Emby.Server.Implementations/Localization/Core/it.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 297b3abce7..e05afbabeb 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/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", From 114591c1aacbdf4d07e95c536ea2e42af1c5ab0d Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Tue, 25 Feb 2025 01:51:38 -0600 Subject: [PATCH 051/508] Clean up usings and honor SortName --- MediaBrowser.Controller/Entities/BaseItem.cs | 1 - .../Probing/ProbeResultNormalizer.cs | 1 - .../MediaInfo/AudioFileProber.cs | 1 - .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 1 - .../Savers/AlbumNfoSaver.cs | 1 + .../Savers/ArtistNfoSaver.cs | 3 +-- .../Savers/BaseNfoSaver.cs | 20 ++++++++++++++++++- .../Savers/MovieNfoSaver.cs | 1 - 8 files changed, 21 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 29b0b6861b..95d0f311e4 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -24,7 +24,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index c6a2ca5a4e..6b0fd9a147 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -12,7 +12,6 @@ using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 0e22dd96ed..b504da48f7 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -14,7 +14,6 @@ using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index ce10d4a8a2..9bb6507fe6 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -12,7 +12,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using TMDbLib.Objects.Find; diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs index 774539c95e..440296f095 100644 --- a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs @@ -74,6 +74,7 @@ namespace MediaBrowser.XbmcMetadata.Savers foreach (var track in tracks .OrderBy(i => i.ParentIndexNumber ?? 0) .ThenBy(i => i.IndexNumber ?? 0) + .ThenBy(i => SortNameOrName(i)) .ThenBy(i => i.Name?.Trim())) { writer.WriteStartElement("track"); diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs index e1d006bfae..b5ba2d24f2 100644 --- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs @@ -7,7 +7,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.IO; using MediaBrowser.XbmcMetadata.Configuration; using Microsoft.Extensions.Logging; @@ -73,7 +72,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { foreach (var album in albums .OrderBy(album => album.ProductionYear ?? 0) - .ThenBy(album => album.SortName?.Trim()) + .ThenBy(album => SortNameOrName(album)) .ThenBy(album => album.Name?.Trim())) { writer.WriteStartElement("album"); diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 9c006e2064..f14bd437a8 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -19,7 +19,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -1038,5 +1037,24 @@ namespace MediaBrowser.XbmcMetadata.Savers private string GetTagForProviderKey(string providerKey) => providerKey.ToLowerInvariant() + "id"; + + protected static string SortNameOrName(BaseItem item) + { + if (item == null) + { + return string.Empty; + } + + if (item.SortName != null) + { + string trimmed = item.SortName.Trim(); + if (trimmed.Length > 0) + { + return trimmed; + } + } + + return (item.Name ?? string.Empty).Trim(); + } } } diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index d119751791..a32491c458 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -9,7 +9,6 @@ using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; From d28ee96f06f57483c24f5cfcb14152c6b79a9c7d Mon Sep 17 00:00:00 2001 From: Lampan-git <22211983+Lampan-git@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:35:28 +0100 Subject: [PATCH 052/508] Include PeopleBaseItemMap in GetPeople --- .../Item/PeopleRepository.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index a8dfd4cd3a..77b00a41a0 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -39,6 +39,30 @@ public class PeopleRepository(IDbContextFactory dbProvider, I dbQuery = dbQuery.Take(filter.Limit); } + // Include PeopleBaseItemMap + if (!filter.ItemId.IsEmpty()) + { + var people = dbQuery.ToArray(); + var peopleIds = people.Select(p => p.Id).ToArray(); + + var mappings = context.PeopleBaseItemMap + .AsNoTracking() + .Where(m => peopleIds.Contains(m.PeopleId) && m.ItemId == filter.ItemId) + .ToDictionary(m => m.PeopleId); + + return people.Select(p => + { + var personInfo = Map(p); + if (mappings.TryGetValue(p.Id, out var mapping)) + { + personInfo.Role = mapping.Role; + personInfo.SortOrder = mapping.SortOrder; + } + + return personInfo; + }).ToArray(); + } + return dbQuery.AsEnumerable().Select(Map).ToArray(); } From 06be4998e189729170a6f6dbbca68a0790c72d43 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Tue, 25 Feb 2025 15:26:47 -0500 Subject: [PATCH 053/508] Backport pull request #13611 from jellyfin/release-10.10.z Remove empty ParentIndexNumber workaround Original-merge: 1daf761aece5114a6ac3b7b938f114e70b83d99e Merged-by: Bond-009 Backported-by: Bond_009 --- Emby.Server.Implementations/Library/LibraryManager.cs | 9 --------- .../Plugins/Omdb/OmdbEpisodeProvider.cs | 5 ++--- .../Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs | 6 +++--- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 10 +++++----- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index c483f3c61f..8568104941 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2630,15 +2630,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) { diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs index d8b33a799f..ccff31ebaa 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs +++ b/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, diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs index d1fec7cb13..7de0e430f2 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -63,10 +63,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV return Enumerable.Empty(); } - var seasonNumber = episode.ParentIndexNumber; + var seasonNumber = episode.ParentIndexNumber ?? 1; var episodeNumber = episode.IndexNumber; - if (!seasonNumber.HasValue || !episodeNumber.HasValue) + if (!episodeNumber.HasValue) { return Enumerable.Empty(); } @@ -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; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index e628abde55..c93dabb66c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV public async Task> 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(); } @@ -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? 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()).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); } From 33e8c18136f4dd380650962a18ebcb3bf2e7f087 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 25 Feb 2025 15:32:02 -0500 Subject: [PATCH 054/508] Backport pull request #13593 from jellyfin/release-10.10.z Wait for ffmpeg to exit on Windows before we try deleting the concat file Original-merge: 346f36bc09eb6989d7cd6439175e46b699162cbb Merged-by: Bond-009 Backported-by: Bond_009 --- .../Tasks/AudioNormalizationTask.cs | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs index 031d147765..8d1d509ff7 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs +++ b/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 CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken) + private async Task 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; } } } From 1131b051d8ec9f5cb0d0b3d42852c1a022a04932 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 25 Feb 2025 15:32:03 -0500 Subject: [PATCH 055/508] Backport pull request #13601 from jellyfin/release-10.10.z Delete children from cache on parent delete Original-merge: 767a5e61930897d6151bf2b5b6c4940a288deb41 Merged-by: Bond-009 Backported-by: Bond_009 --- Emby.Server.Implementations/Library/LibraryManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 8568104941..cc2092e21e 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -457,6 +457,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 _); From c38e887ea5235a6a485417ff5224a6da80209d68 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 00:31:28 +0000 Subject: [PATCH 056/508] Update actions/download-artifact action to v4.1.9 --- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/ci-openapi.yml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 1975a9f03a..ca505790cc 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -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 diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index bc30256568..85a7a33bcd 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -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 @@ -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 From 0803600afdf06a10b1bb0d40c992bb3aa0cda4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Mesk=C3=B3?= Date: Tue, 25 Feb 2025 10:21:33 +0000 Subject: [PATCH 057/508] Translated using Weblate (Hungarian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/ --- Emby.Server.Implementations/Localization/Core/hu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index f205e8b64c..1a9c3ee8be 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/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", From 93dd5551df280f53eaa441156cc6016ee384e554 Mon Sep 17 00:00:00 2001 From: Dominik Krivohlavek Date: Sat, 1 Mar 2025 07:00:52 +0100 Subject: [PATCH 058/508] Add support for reading and storing Recording MBIDs from file metadata (#12173) * Add recording metadata provider * Add recording MBID * Save recording MBID during probing * Set recording ID in probe result normalizer * Add recording external media type * Reimplement after changes in upstream * Rename variable * Rename variable * Revert "Set recording ID in probe result normalizer" This reverts commit 9dd18c8aba3f970a5816a13a33acf3d58b0e440f. * Fix setting provider ID * Simplify code * Fix comment * Add missing using --- .../Entities/MetadataProvider.cs | 7 ++++- .../Providers/ExternalIdMediaType.cs | 7 ++++- .../MediaInfo/AudioFileProber.cs | 19 +++++++++++++ .../MusicBrainz/MusicBrainzRecordingId.cs | 27 +++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs diff --git a/MediaBrowser.Model/Entities/MetadataProvider.cs b/MediaBrowser.Model/Entities/MetadataProvider.cs index dcc4ae88c5..65337b60fd 100644 --- a/MediaBrowser.Model/Entities/MetadataProvider.cs +++ b/MediaBrowser.Model/Entities/MetadataProvider.cs @@ -84,6 +84,11 @@ namespace MediaBrowser.Model.Entities /// /// The TvMaze provider. /// - TvMaze = 19 + TvMaze = 19, + + /// + /// The MusicBrainz recording provider. + /// + MusicBrainzRecording = 20, } } diff --git a/MediaBrowser.Model/Providers/ExternalIdMediaType.cs b/MediaBrowser.Model/Providers/ExternalIdMediaType.cs index ef518369cc..71a131bb80 100644 --- a/MediaBrowser.Model/Providers/ExternalIdMediaType.cs +++ b/MediaBrowser.Model/Providers/ExternalIdMediaType.cs @@ -71,6 +71,11 @@ namespace MediaBrowser.Model.Providers /// /// A book. /// - Book = 13 + Book = 13, + + /// + /// A music recording. + /// + Recording = 14 } } diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index a0481a6426..963b611515 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/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' + 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; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs new file mode 100644 index 0000000000..d2af628067 --- /dev/null +++ b/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; + +/// +/// MusicBrainz recording id. +/// +public class MusicBrainzRecordingId : IExternalId +{ + /// + public string ProviderName => "MusicBrainz"; + + /// + public string Key => MetadataProvider.MusicBrainzRecording.ToString(); + + /// + public ExternalIdMediaType? Type => ExternalIdMediaType.Recording; + + /// + public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/recording/{0}"; + + /// + public bool Supports(IHasProviderIds item) => item is Audio; +} From f035b1162528ed4c351ae03f2c217499e46bdd1a Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Sat, 1 Mar 2025 00:01:21 -0600 Subject: [PATCH 059/508] Better exception message when folders or folder items are missing (#13632) Emit the not-found Id in the exception for easier diagnosis --- Emby.Server.Implementations/Collections/CollectionManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index e414792ba0..4a0662e16a 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/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? 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)) From a6b4d124d71c0bbb9dff5f226e65875e03384ab4 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 1 Mar 2025 14:16:02 +0000 Subject: [PATCH 060/508] Replicated changes made from #13492 --- .../IJellyfinDatabaseProvider.cs | 6 ++++++ .../JellyfinDbContext.cs | 7 +++++++ .../DoNotUseReturningClauseConvention.cs | 20 +++++++++++++++++++ .../SqliteDatabaseProvider.cs | 6 ++++++ 4 files changed, 39 insertions(+) create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/DoNotUseReturningClauseConvention.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs index b27a88971d..cc96792e64 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs @@ -27,6 +27,12 @@ public interface IJellyfinDatabaseProvider /// The ModelBuilder from EFCore. void OnModelCreating(ModelBuilder modelBuilder); + /// + /// Will be invoked when EFCore wants to configure its model. + /// + /// The ModelConfigurationBuilder from EFCore. + void ConfigureConventions(ModelConfigurationBuilder configurationBuilder); + /// /// If supported this should run any periodic maintaince tasks. /// diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs index a0a0f2d0ee..c65006c7d8 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs @@ -272,4 +272,11 @@ public class JellyfinDbContext(DbContextOptions options, ILog // Configuration for each entity is in its own class inside 'ModelConfiguration'. modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDbContext).Assembly); } + + /// + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + jellyfinDatabaseProvider.ConfigureConventions(configurationBuilder); + base.ConfigureConventions(configurationBuilder); + } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/DoNotUseReturningClauseConvention.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/DoNotUseReturningClauseConvention.cs new file mode 100644 index 0000000000..1ce2420e43 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/DoNotUseReturningClauseConvention.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; + +namespace Jellyfin.Database.Providers.SqLite; + +internal class DoNotUseReturningClauseConvention : IModelFinalizingConvention +{ + /// + public void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + entityType.UseSqlReturningClause(false); + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs index f7fde4989e..2364186b12 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs @@ -78,4 +78,10 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider SqliteConnection.ClearAllPools(); } + + /// + public void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + configurationBuilder.Conventions.Add(_ => new DoNotUseReturningClauseConvention()); + } } From 82b3135dd9eaa2cc0ef29d82d8cb7196d4724394 Mon Sep 17 00:00:00 2001 From: Zero King Date: Sun, 2 Mar 2025 01:03:55 +0800 Subject: [PATCH 061/508] Fix possible NullReferenceException in playlist warning --- Emby.Server.Implementations/Playlists/PlaylistManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index daeb7fed88..9e780a49e5 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -310,7 +310,7 @@ namespace Emby.Server.Implementations.Playlists var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)); if (item is null) { - _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId); + _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", entryId, playlistId); return; } From aad7506e854e5118bf70a5f103e2128f765ea7f2 Mon Sep 17 00:00:00 2001 From: Lampan-git Date: Sun, 2 Mar 2025 11:23:01 -0500 Subject: [PATCH 062/508] Backport pull request #13618 from jellyfin/release-10.10.z Include Role and SortOrder in MergePeople to fix "Search for missing metadata" Original-merge: fcdef875a2b0e49bc0ebeec12797c91ddb8f9bdc Merged-by: Bond-009 Backported-by: Bond_009 --- MediaBrowser.Providers/Manager/MetadataService.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 778fbc7125..1d3ddc4e24 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -1162,6 +1162,16 @@ namespace MediaBrowser.Providers.Manager { person.ImageUrl = personInSource.ImageUrl; } + + if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role)) + { + person.Role = personInSource.Role; + } + + if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue) + { + person.SortOrder = personInSource.SortOrder; + } } } } From efb901c36976c006f2b9ad8420a06181819c9016 Mon Sep 17 00:00:00 2001 From: IDisposable Date: Sun, 2 Mar 2025 11:23:02 -0500 Subject: [PATCH 063/508] Backport pull request #13639 from jellyfin/release-10.10.z Support more rating formats Original-merge: 4f94d23011c4af755e6e05cc42f47befc7e43fcb Merged-by: Bond-009 Backported-by: Bond_009 --- .../Localization/LocalizationManager.cs | 6 ++++-- .../Localization/LocalizationManagerTests.cs | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index c939a5e099..754a01329b 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -286,8 +286,10 @@ namespace Emby.Server.Implementations.Localization } // Fairly common for some users to have "Rated R" in their rating field - rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase); - rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase); + rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("Rated:", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase) + .Trim(); // Use rating system matching the language if (!string.IsNullOrEmpty(countryCode)) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 65f018ee3f..cc67dbc397 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Runtime.InteropServices; using System.Threading.Tasks; using Emby.Server.Implementations.Localization; using MediaBrowser.Controller.Configuration; @@ -116,6 +115,10 @@ namespace Jellyfin.Server.Implementations.Tests.Localization [InlineData("TV-MA", "US", 17)] [InlineData("XXX", "asdf", 1000)] [InlineData("Germany: FSK-18", "DE", 18)] + [InlineData("Rated : R", "US", 17)] + [InlineData("Rated: R", "US", 17)] + [InlineData("Rated R", "US", 17)] + [InlineData(" PG-13 ", "US", 13)] public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel) { var localizationManager = Setup(new ServerConfiguration() From 728819780a81cc8146d18d0605c8ad6ce00c522f Mon Sep 17 00:00:00 2001 From: "Troj@" Date: Tue, 4 Mar 2025 12:59:08 +0000 Subject: [PATCH 064/508] Translated using Weblate (Belarusian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/ --- Emby.Server.Implementations/Localization/Core/be.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 97aa0ca58c..9e62c8a74c 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -16,7 +16,7 @@ "Collections": "Калекцыі", "Default": "Па змаўчанні", "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}", - "Folders": "Папкі", + "Folders": "Тэчкі", "Favorites": "Абранае", "External": "Знешні", "Genres": "Жанры", From ab369f27f7793c7fa7f482b71a7cdca5b8c54ee7 Mon Sep 17 00:00:00 2001 From: "Troj@" Date: Tue, 4 Mar 2025 18:49:44 +0000 Subject: [PATCH 065/508] Translated using Weblate (Belarusian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/ --- Emby.Server.Implementations/Localization/Core/be.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 9e62c8a74c..d5da04fb9f 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -1,6 +1,6 @@ { "Sync": "Сінхранізаваць", - "Playlists": "Плэйлісты", + "Playlists": "Спісы прайгравання", "Latest": "Апошні", "LabelIpAddressValue": "IP-адрас: {0}", "ItemAddedWithName": "{0} быў дададзены ў бібліятэку", From 4e3d7383f5cb85eb408f9d026b6da1986925df17 Mon Sep 17 00:00:00 2001 From: Lampan-git <22211983+Lampan-git@users.noreply.github.com> Date: Wed, 5 Mar 2025 16:59:31 +0100 Subject: [PATCH 066/508] Change GetPeople PeopleBaseItemMap code to query --- .../Item/PeopleRepository.cs | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 77b00a41a0..cca25de731 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -42,25 +42,23 @@ public class PeopleRepository(IDbContextFactory dbProvider, I // Include PeopleBaseItemMap if (!filter.ItemId.IsEmpty()) { - var people = dbQuery.ToArray(); - var peopleIds = people.Select(p => p.Id).ToArray(); + var query = dbQuery + .GroupJoin( + context.PeopleBaseItemMap.AsNoTracking().Where(m => m.ItemId == filter.ItemId), + person => person.Id, + mapping => mapping.PeopleId, + (person, mappings) => new { Person = person, Mapping = mappings.FirstOrDefault() }); - var mappings = context.PeopleBaseItemMap - .AsNoTracking() - .Where(m => peopleIds.Contains(m.PeopleId) && m.ItemId == filter.ItemId) - .ToDictionary(m => m.PeopleId); - - return people.Select(p => - { - var personInfo = Map(p); - if (mappings.TryGetValue(p.Id, out var mapping)) + return query + .AsEnumerable() + .Select(p => { - personInfo.Role = mapping.Role; - personInfo.SortOrder = mapping.SortOrder; - } - - return personInfo; - }).ToArray(); + var personInfo = Map(p.Person); + personInfo.Role = p.Mapping?.Role; + personInfo.SortOrder = p.Mapping?.SortOrder; + return personInfo; + }) + .ToArray(); } return dbQuery.AsEnumerable().Select(Map).ToArray(); From e137a063623daa421c34fc7d27ac2502d66b8e0b Mon Sep 17 00:00:00 2001 From: Lampan-git <22211983+Lampan-git@users.noreply.github.com> Date: Wed, 5 Mar 2025 16:58:34 +0100 Subject: [PATCH 067/508] Change PeopleBaseItemMap query from GroupJoin to Include --- .../Item/PeopleRepository.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index cca25de731..28909cea78 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -42,20 +42,16 @@ public class PeopleRepository(IDbContextFactory dbProvider, I // Include PeopleBaseItemMap if (!filter.ItemId.IsEmpty()) { - var query = dbQuery - .GroupJoin( - context.PeopleBaseItemMap.AsNoTracking().Where(m => m.ItemId == filter.ItemId), - person => person.Id, - mapping => mapping.PeopleId, - (person, mappings) => new { Person = person, Mapping = mappings.FirstOrDefault() }); + dbQuery = dbQuery.Include(p => p.BaseItems!.Where(m => m.ItemId == filter.ItemId)); - return query + return dbQuery .AsEnumerable() .Select(p => { - var personInfo = Map(p.Person); - personInfo.Role = p.Mapping?.Role; - personInfo.SortOrder = p.Mapping?.SortOrder; + var personInfo = Map(p); + var mapping = p.BaseItems?.FirstOrDefault(); + personInfo.Role = mapping?.Role; + personInfo.SortOrder = mapping?.SortOrder; return personInfo; }) .ToArray(); From 7abb94d8a20072d451d6f58a0daa427efb5a93bf Mon Sep 17 00:00:00 2001 From: Lampan-git <22211983+Lampan-git@users.noreply.github.com> Date: Wed, 5 Mar 2025 22:37:18 +0100 Subject: [PATCH 068/508] Move mapping assignment to Map --- .../Item/PeopleRepository.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 28909cea78..1396f1c6f8 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -43,18 +43,6 @@ public class PeopleRepository(IDbContextFactory dbProvider, I if (!filter.ItemId.IsEmpty()) { dbQuery = dbQuery.Include(p => p.BaseItems!.Where(m => m.ItemId == filter.ItemId)); - - return dbQuery - .AsEnumerable() - .Select(p => - { - var personInfo = Map(p); - var mapping = p.BaseItems?.FirstOrDefault(); - personInfo.Role = mapping?.Role; - personInfo.SortOrder = mapping?.SortOrder; - return personInfo; - }) - .ToArray(); } return dbQuery.AsEnumerable().Select(Map).ToArray(); @@ -111,10 +99,13 @@ public class PeopleRepository(IDbContextFactory dbProvider, I private PersonInfo Map(People people) { + var mapping = people.BaseItems?.FirstOrDefault(); var personInfo = new PersonInfo() { Id = people.Id, Name = people.Name, + Role = mapping?.Role, + SortOrder = mapping?.SortOrder }; if (Enum.TryParse(people.PersonType, out var kind)) { From 70b8fa73f081ba406a16ccb17bdc7c8c7a39f2b1 Mon Sep 17 00:00:00 2001 From: Roman Dordzheev Date: Sat, 8 Mar 2025 13:55:21 +0300 Subject: [PATCH 069/508] Include SortName in LibraryDb migration query --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 3289484f93..e3f5b18e7d 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -89,7 +89,7 @@ public class MigrateLibraryDb : IMigrationRoutine Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId, PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, - ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType FROM TypedBaseItems + ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName FROM TypedBaseItems """; dbContext.BaseItems.ExecuteDelete(); @@ -1034,6 +1034,11 @@ public class MigrateLibraryDb : IMigrationRoutine entity.MediaType = mediaType; } + if (reader.TryGetString(index++, out var sortName)) + { + entity.SortName = sortName; + } + var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false); var dataKeys = baseItem.GetUserDataKeys(); userDataKeys.AddRange(dataKeys); From cb650c69b813c2d0fef1a6fca6d42b81af3f2e12 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 9 Mar 2025 12:45:55 +0000 Subject: [PATCH 070/508] Update dependency z440.atl.core to 6.19.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d23d704d5b..7e04a7326e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -79,7 +79,7 @@ - + From f5adbc029636f1555ee9b425f2ac40d72a433c82 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 9 Mar 2025 17:00:13 +0000 Subject: [PATCH 071/508] Update CI dependencies --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- .github/workflows/ci-openapi.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 1f166d10c8..850214fec1 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/autobuild@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 85a7a33bcd..5a32849877 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -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@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1 + uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" @@ -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@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1 + uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" From 8ef7b4f9b5792f5a669f3f15e90697a9a90121b2 Mon Sep 17 00:00:00 2001 From: Thunderstrike116 Date: Sun, 9 Mar 2025 20:06:30 +0000 Subject: [PATCH 072/508] Translated using Weblate (Greek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/ --- Emby.Server.Implementations/Localization/Core/el.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 55f266032e..619c61d7de 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -96,7 +96,7 @@ "TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.", "TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής", "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.", - "TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων", + "TaskRefreshLibrary": "Σάρωση Βιβλιοθήκης Πολυμέσων", "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.", "TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου", "TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.", From 0d7eb489309eb0bca00854cc0dddd5e19c332067 Mon Sep 17 00:00:00 2001 From: Thunderstrike116 Date: Sun, 9 Mar 2025 20:18:30 +0000 Subject: [PATCH 073/508] Translated using Weblate (Greek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/ --- Emby.Server.Implementations/Localization/Core/el.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 619c61d7de..53e094bfc7 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -28,7 +28,7 @@ "HomeVideos": "Προσωπικά Βίντεο", "Inherit": "Κληρονόμηση", "ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη", - "ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη", + "ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη", "LabelIpAddressValue": "Διεύθυνση IP: {0}", "LabelRunningTimeValue": "Διάρκεια: {0}", "Latest": "Πρόσφατα", From de5b6470beef2497d268de15c9c91f57f6203d0f Mon Sep 17 00:00:00 2001 From: congerh Date: Mon, 10 Mar 2025 06:56:51 -0400 Subject: [PATCH 074/508] Backport pull request #13659 from jellyfin/release-10.10.z Upgrade LrcParser to 2025.228.1 Original-merge: ae6a7acf1465572fb00fb49629ca1e78fab2f8f9 Merged-by: Bond-009 Backported-by: Bond_009 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7e04a7326e..7ae2ec3ab9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,7 +22,7 @@ - + From f1dd065eca88f6ee5a1d162587fd207da64aa679 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Mon, 10 Mar 2025 11:50:28 -0400 Subject: [PATCH 075/508] Include CleanName in LibraryDb migration query --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index e3f5b18e7d..dfe497c490 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -89,7 +89,7 @@ public class MigrateLibraryDb : IMigrationRoutine Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId, PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, - ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName FROM TypedBaseItems + ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName FROM TypedBaseItems """; dbContext.BaseItems.ExecuteDelete(); @@ -1039,6 +1039,11 @@ public class MigrateLibraryDb : IMigrationRoutine entity.SortName = sortName; } + if (reader.TryGetString(index++, out var cleanName)) + { + entity.CleanName = cleanName; + } + var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false); var dataKeys = baseItem.GetUserDataKeys(); userDataKeys.AddRange(dataKeys); From 4325c67e89a6ad86e559ced936c2b4c20ee89110 Mon Sep 17 00:00:00 2001 From: "denys.kozhevnikov" Date: Mon, 10 Mar 2025 16:15:31 +0000 Subject: [PATCH 076/508] Add ability to provide search pattern --- .../IO/ManagedFileSystem.cs | 26 +++++++++++++---- MediaBrowser.Model/IO/IFileSystem.cs | 28 +++++++++++++++++++ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 66b7839f77..ac5933a694 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -541,8 +541,8 @@ namespace Emby.Server.Implementations.IO return DriveInfo.GetDrives() .Where( d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable) - && d.IsReady - && d.TotalSize != 0) + && d.IsReady + && d.TotalSize != 0) .Select(d => new FileSystemMetadata { Name = d.Name, @@ -560,11 +560,23 @@ namespace Emby.Server.Implementations.IO /// public virtual IEnumerable GetFiles(string path, bool recursive = false) { - return GetFiles(path, null, false, recursive); + return GetFiles(path, "*", recursive); } /// - public virtual IEnumerable GetFiles(string path, IReadOnlyList? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) + public virtual IEnumerable GetFiles(string path, string searchPattern, bool recursive = false) + { + return GetFiles(path, searchPattern, null, false, recursive); + } + + /// + public virtual IEnumerable GetFiles(string path, IReadOnlyList? extensions, bool enableCaseSensitiveExtensions, bool recursive) + { + return GetFiles(path, "*", extensions, enableCaseSensitiveExtensions, recursive); + } + + /// + public virtual IEnumerable GetFiles(string path, string searchPattern, IReadOnlyList? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { var enumerationOptions = GetEnumerationOptions(recursive); @@ -572,10 +584,12 @@ namespace Emby.Server.Implementations.IO // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1) { - return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions)); + searchPattern = searchPattern.EndsWith(extensions[0], StringComparison.Ordinal) ? searchPattern : searchPattern + extensions[0]; + + return ToMetadata(new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions)); } - var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions); + var files = new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions); if (extensions is not null && extensions.Count > 0) { diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index 229368d004..0ed2e30d52 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -157,8 +157,36 @@ namespace MediaBrowser.Model.IO /// All found files. IEnumerable GetFiles(string path, bool recursive = false); + /// + /// Gets the files. + /// + /// The path in which to search. + /// The search string to match against the names of files. This parameter can contain a combination of valid literal path and wildcard (* and ?) characters, but it doesn't support regular expressions. + /// If set to true also searches in subdirectories. + /// All found files. + IEnumerable GetFiles(string path, string searchPattern, bool recursive = false); + + /// + /// Gets the files. + /// + /// The path in which to search. + /// The file extensions to search for. + /// Enable case-sensitive check for extensions. + /// If set to true also searches in subdirectories. + /// All found files. IEnumerable GetFiles(string path, IReadOnlyList? extensions, bool enableCaseSensitiveExtensions, bool recursive); + /// + /// Gets the files. + /// + /// The path in which to search. + /// The search string to match against the names of files. This parameter can contain a combination of valid literal path and wildcard (* and ?) characters, but it doesn't support regular expressions. + /// The file extensions to search for. + /// Enable case-sensitive check for extensions. + /// If set to true also searches in subdirectories. + /// All found files. + IEnumerable GetFiles(string path, string searchPattern, IReadOnlyList? extensions, bool enableCaseSensitiveExtensions, bool recursive); + /// /// Gets the file system entries. /// From 490e087b46b50a4702a43368c6a4b2d3183ea5ec Mon Sep 17 00:00:00 2001 From: Thunderstrike116 Date: Tue, 11 Mar 2025 17:11:33 +0000 Subject: [PATCH 077/508] Translated using Weblate (Greek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/ --- Emby.Server.Implementations/Localization/Core/el.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 53e094bfc7..24502c8199 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -11,7 +11,7 @@ "Collections": "Συλλογές", "DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε", "DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε", - "FailedLoginAttemptWithUserName": "Αποτυχημένη προσπάθεια σύνδεσης από {0}", + "FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}", "Favorites": "Αγαπημένα", "Folders": "Φάκελοι", "Genres": "Είδη", From 237e7bd44b3c9a6f76892be1c6a925bcde64bdbf Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 12 Mar 2025 08:40:33 -0400 Subject: [PATCH 078/508] Backport pull request #13694 from jellyfin/release-10.10.z Clone fallback audio tags instead of use ATL.Track.set Original-merge: 9eb2044eae50c69be4cb3830887bdd5da15ee920 Merged-by: Bond-009 Backported-by: Bond_009 --- .../MediaInfo/AudioFileProber.cs | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 963b611515..8a259ac541 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -175,11 +175,15 @@ namespace MediaBrowser.Providers.MediaInfo _logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path); } - track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title; - track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album; - track.Year = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year; - track.TrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber; - track.DiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber; + // We should never use the property setter of the ATL.Track class. + // That setter is meant for its own tag parser and external editor usage and will have unwanted side effects + // For example, setting the Year property will also set the Date property, which is not what we want here. + // To properly handle fallback values, we make a clone of those fields when valid. + var trackTitle = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title; + var trackAlbum = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album; + var trackYear = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year; + var trackTrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber; + var trackDiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber; if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast)) { @@ -276,22 +280,22 @@ namespace MediaBrowser.Providers.MediaInfo } } - if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(track.Title)) + if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(trackTitle)) { - audio.Name = track.Title; + audio.Name = trackTitle; } if (options.ReplaceAllMetadata) { - audio.Album = track.Album; - audio.IndexNumber = track.TrackNumber; - audio.ParentIndexNumber = track.DiscNumber; + audio.Album = trackAlbum; + audio.IndexNumber = trackTrackNumber; + audio.ParentIndexNumber = trackDiscNumber; } else { - audio.Album ??= track.Album; - audio.IndexNumber ??= track.TrackNumber; - audio.ParentIndexNumber ??= track.DiscNumber; + audio.Album ??= trackAlbum; + audio.IndexNumber ??= trackTrackNumber; + audio.ParentIndexNumber ??= trackDiscNumber; } if (track.Date.HasValue) @@ -299,11 +303,12 @@ namespace MediaBrowser.Providers.MediaInfo audio.PremiereDate = track.Date; } - if (track.Year.HasValue) + if (trackYear.HasValue) { - var year = track.Year.Value; + var year = trackYear.Value; audio.ProductionYear = year; + // ATL library handles such fallback this with its own internal logic, but we also need to handle it here for the ffprobe fallbacks. if (!audio.PremiereDate.HasValue) { try @@ -312,7 +317,7 @@ namespace MediaBrowser.Providers.MediaInfo } catch (ArgumentOutOfRangeException ex) { - _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, track.Year); + _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, trackYear); } } } From cbca153132572a7816d49962c2f6cfda0ab8ae9b Mon Sep 17 00:00:00 2001 From: gnattu Date: Thu, 13 Mar 2025 06:27:12 +0800 Subject: [PATCH 079/508] More typos --- .../Manager/NetworkManager.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 15b3edbd59..e14daadc75 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -692,7 +692,7 @@ public class NetworkManager : INetworkManager, IDisposable if (_remoteAddressFilter.Any() && !IsInLocalNetwork(remoteIP)) { // remoteAddressFilter is a whitelist or blacklist. - var matches = _remoteAddressFilter.Count(remoteNetwork => NetworkUtils.SubNetContainsAddress(remoteNetwork, remoteIP)); + var matches = _remoteAddressFilter.Count(remoteNetwork => NetworkUtils.SubnetContainsAddress(remoteNetwork, remoteIP)); if ((!config.IsRemoteIPFilterBlacklist && matches > 0) || (config.IsRemoteIPFilterBlacklist && matches == 0)) { @@ -863,7 +863,7 @@ public class NetworkManager : INetworkManager, IDisposable // (For systems with multiple internal network cards, and multiple subnets) foreach (var intf in availableInterfaces) { - if (NetworkUtils.SubNetContainsAddress(intf.Subnet, source)) + if (NetworkUtils.SubnetContainsAddress(intf.Subnet, source)) { result = NetworkUtils.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result); @@ -974,7 +974,7 @@ public class NetworkManager : INetworkManager, IDisposable { // Only use matching internal subnets // Prefer more specific (bigger subnet prefix) overrides - validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && NetworkUtils.SubNetContainsAddress(x.Data.Subnet, source)) + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && NetworkUtils.SubnetContainsAddress(x.Data.Subnet, source)) .OrderByDescending(x => x.Data.Subnet.PrefixLength) .ToList(); } @@ -982,7 +982,7 @@ public class NetworkManager : INetworkManager, IDisposable { // Only use matching external subnets // Prefer more specific (bigger subnet prefix) overrides - validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && NetworkUtils.SubNetContainsAddress(x.Data.Subnet, source)) + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && NetworkUtils.SubnetContainsAddress(x.Data.Subnet, source)) .OrderByDescending(x => x.Data.Subnet.PrefixLength) .ToList(); } @@ -990,7 +990,7 @@ public class NetworkManager : INetworkManager, IDisposable foreach (var data in validPublishedServerUrls) { // Get interface matching override subnet - var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => NetworkUtils.SubNetContainsAddress(data.Data.Subnet, x.Address)); + var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => NetworkUtils.SubnetContainsAddress(data.Data.Subnet, x.Address)); if (intf?.Address is not null || (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any)) @@ -1061,7 +1061,7 @@ public class NetworkManager : INetworkManager, IDisposable // Check to see if any of the external bind interfaces are in the same subnet as the source. // If none exists, this will select the first external interface if there is one. bindAddress = externalInterfaces - .OrderByDescending(x => NetworkUtils.SubNetContainsAddress(x.Subnet, source)) + .OrderByDescending(x => NetworkUtils.SubnetContainsAddress(x.Subnet, source)) .ThenByDescending(x => x.Subnet.PrefixLength) .ThenBy(x => x.Index) .Select(x => x.Address) @@ -1079,7 +1079,7 @@ public class NetworkManager : INetworkManager, IDisposable // Check to see if any of the internal bind interfaces are in the same subnet as the source. // If none exists, this will select the first internal interface if there is one. bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address)) - .OrderByDescending(x => NetworkUtils.SubNetContainsAddress(x.Subnet, source)) + .OrderByDescending(x => NetworkUtils.SubnetContainsAddress(x.Subnet, source)) .ThenByDescending(x => x.Subnet.PrefixLength) .ThenBy(x => x.Index) .Select(x => x.Address) @@ -1123,7 +1123,7 @@ public class NetworkManager : INetworkManager, IDisposable // (For systems with multiple network cards and/or multiple subnets) foreach (var intf in extResult) { - if (NetworkUtils.SubNetContainsAddress(intf.Subnet, source)) + if (NetworkUtils.SubnetContainsAddress(intf.Subnet, source)) { result = NetworkUtils.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result); From 8cb5ea60d68bb49350a310857d043b67e10ab8b7 Mon Sep 17 00:00:00 2001 From: Thunderstrike116 Date: Wed, 12 Mar 2025 16:55:01 +0000 Subject: [PATCH 080/508] Translated using Weblate (Greek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/ --- .../Localization/Core/el.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 24502c8199..631e659d5c 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -27,7 +27,7 @@ "HeaderRecordingGroups": "Ομάδες Ηχογράφησης", "HomeVideos": "Προσωπικά Βίντεο", "Inherit": "Κληρονόμηση", - "ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη", + "ItemAddedWithName": "Το {0} προστέθηκε στη βιβλιοθήκη", "ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη", "LabelIpAddressValue": "Διεύθυνση IP: {0}", "LabelRunningTimeValue": "Διάρκεια: {0}", @@ -40,7 +40,7 @@ "Movies": "Ταινίες", "Music": "Μουσική", "MusicVideos": "Μουσικά Βίντεο", - "NameInstallFailed": "{0} η εγκατάσταση απέτυχε", + "NameInstallFailed": "H εγκατάσταση του {0} απέτυχε", "NameSeasonNumber": "Κύκλος {0}", "NameSeasonUnknown": "Άγνωστος Κύκλος", "NewVersionIsAvailable": "Μια νέα έκδοση του διακομιστή Jellyfin είναι διαθέσιμη για λήψη.", @@ -54,7 +54,7 @@ "NotificationOptionPluginError": "Αποτυχία του πρόσθετου", "NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε", "NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε", - "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του πρόσθετου εγκαταστάθηκε", + "NotificationOptionPluginUpdateInstalled": "Η ενημέρωση του πρόσθετου εγκαταστάθηκε", "NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση", "NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας", "NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε", @@ -63,9 +63,9 @@ "Photos": "Φωτογραφίες", "Playlists": "Λίστες αναπαραγωγής", "Plugin": "Πρόσθετο", - "PluginInstalledWithName": "{0} εγκαταστήθηκε", - "PluginUninstalledWithName": "{0} έχει απεγκατασταθεί", - "PluginUpdatedWithName": "{0} έχει αναβαθμιστεί", + "PluginInstalledWithName": "Το {0} εγκαταστάθηκε", + "PluginUninstalledWithName": "Το {0} έχει απεγκατασταθεί", + "PluginUpdatedWithName": "Το {0} ενημερώθηκε", "ProviderValue": "Πάροχος: {0}", "ScheduledTaskFailedWithName": "{0} αποτυχία", "ScheduledTaskStartedWithName": "{0} ξεκίνησε", From b346d12e1c1c15e39a131f1ad4efb530c5d38c32 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:36:21 -0600 Subject: [PATCH 081/508] Update Microsoft to 9.0.3 (#13702) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 50 ++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7ae2ec3ab9..89311142cd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,29 +24,29 @@ - - + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -75,9 +75,9 @@ - - - + + + From 14e3b2214ac7ea64ff6412b8363c9e4d3091f51f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:36:31 -0600 Subject: [PATCH 082/508] Update dependency dotnet-ef to 9.0.3 (#13703) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index ea2675a3d0..bc2098a53b 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "9.0.2", + "version": "9.0.3", "commands": [ "dotnet-ef" ] From 7d6bf5cb0d1ee2f22e9ac96985f93dc9c7f72e81 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:36:41 -0600 Subject: [PATCH 083/508] Update dependency python to 3.13 (#13701) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/commands.yml | 2 +- .github/workflows/issue-template-check.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 1ab7ae029d..082084ed4f 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -46,7 +46,7 @@ jobs: - name: install python uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: - python-version: '3.12' + python-version: '3.13' cache: 'pip' - name: install python packages run: pip install -r rename/requirements.txt diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml index 3c5ba68f91..e3e8019568 100644 --- a/.github/workflows/issue-template-check.yml +++ b/.github/workflows/issue-template-check.yml @@ -16,7 +16,7 @@ jobs: - name: install python uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: - python-version: '3.12' + python-version: '3.13' cache: 'pip' - name: install python packages run: pip install -r main-repo-triage/requirements.txt From 0eed5ee79b12b00426bd18d9830271c4a7f841cc Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Fri, 14 Mar 2025 15:17:18 +0100 Subject: [PATCH 084/508] Fix build and tests (#13718) --- .../MusicBrainz/MusicBrainzRecordingId.cs | 3 -- .../MediaInfo/MediaInfoResolverTests.cs | 52 ++++++++----------- 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs index d2af628067..89d8b9b998 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs @@ -19,9 +19,6 @@ public class MusicBrainzRecordingId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.Recording; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/recording/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio; } diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs index db427308c2..222e624aa2 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs @@ -217,68 +217,58 @@ public class MediaInfoResolverTests string file = "My.Video.srt"; data.Add( file, - new[] - { + [ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0) - }, - new[] - { + ], + [ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0) - }); + ]); // filename has metadata file = "My.Video.Title1.default.forced.sdh.en.srt"; data.Add( file, - new[] - { + [ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0) - }, - new[] - { + ], + [ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title1", 0, true, true, true) - }); + ]); // single stream with metadata file = "My.Video.mks"; data.Add( file, - new[] - { + [ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true, true) - }, - new[] - { - CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true, true) - }); + ], + [ + CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, false, true) + ]); // stream wins for title/language, filename wins for flags when conflicting file = "My.Video.Title2.default.forced.sdh.en.srt"; data.Add( file, - new[] - { + [ CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0) - }, - new[] - { + ], + [ CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0, true, true, true) - }); + ]); // multiple stream with metadata - filename flags ignored but other data filled in when missing from stream file = "My.Video.Title3.default.forced.en.srt"; data.Add( file, - new[] - { + [ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0, true, true), CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1) - }, - new[] - { + ], + [ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title3", 0, true, true), CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1) - }); + ]); return data; } From cf1f251f2a2115c84539f41603252a6733e02482 Mon Sep 17 00:00:00 2001 From: Lampan-git <22211983+Lampan-git@users.noreply.github.com> Date: Fri, 14 Mar 2025 21:07:34 +0100 Subject: [PATCH 085/508] Preserve null sortOrder during migration --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 3289484f93..f826131fc4 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -240,9 +240,7 @@ public class MigrateLibraryDb : IMigrationRoutine { } - if (reader.TryGetInt32(4, out var sortOrder)) - { - } + int? sortOrder = reader.IsDBNull(4) ? null : reader.GetInt32(4); personCache.Items.Add(new PeopleBaseItemMap() { From e684f26c9732352fea948971706287ad36126ec4 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 15 Mar 2025 15:35:08 +0100 Subject: [PATCH 086/508] Add start index to /Programs/Recommended endpoint (#13696) --- Jellyfin.Api/Controllers/LiveTvController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index a3b4c87004..1c0a6af79d 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -698,6 +698,7 @@ public class LiveTvController : BaseJellyfinApiController /// Gets recommended live tv epgs. /// /// Optional. filter by user id. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. /// Optional. The maximum number of records to return. /// Optional. Filter by programs that are currently airing, or not. /// Optional. Filter by programs that have completed airing, or not. @@ -720,6 +721,7 @@ public class LiveTvController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetRecommendedPrograms( [FromQuery] Guid? userId, + [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool? isAiring, [FromQuery] bool? hasAired, @@ -744,6 +746,7 @@ public class LiveTvController : BaseJellyfinApiController var query = new InternalItemsQuery(user) { IsAiring = isAiring, + StartIndex = startIndex, Limit = limit, HasAired = hasAired, IsSeries = isSeries, From 6104d8d5f9f8958878a1e108b59c66c06d3f162b Mon Sep 17 00:00:00 2001 From: Joesph boukolos Date: Sun, 16 Mar 2025 04:29:47 +0000 Subject: [PATCH 087/508] Translated using Weblate (Esperanto) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/eo/ --- Emby.Server.Implementations/Localization/Core/eo.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json index 0b595c2caf..42cce1096f 100644 --- a/Emby.Server.Implementations/Localization/Core/eo.json +++ b/Emby.Server.Implementations/Localization/Core/eo.json @@ -122,5 +122,9 @@ "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis", "TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.", "TaskKeyframeExtractor": "Eltiri Ĉefkadrojn", - "External": "Ekstera" + "External": "Ekstera", + "TaskAudioNormalizationDescription": "Skanas dosierojn por sonnivelaj normaligaj datumoj.", + "TaskRefreshTrickplayImages": "Generi la bildojn por TrickPlay (Antaŭrigardo rapida antaŭen)", + "TaskAudioNormalization": "Normaligo Sonnivela", + "HearingImpaired": "Surda" } From 407935d18143e87c64b0e557aba00d3b2699f151 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 16 Mar 2025 19:00:00 -0400 Subject: [PATCH 088/508] Fix IMDb URL for People --- MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs index ff8ad1d612..980bac102e 100644 --- a/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs @@ -19,7 +19,14 @@ public class ImdbExternalUrlProvider : IExternalUrlProvider var baseUrl = "https://www.imdb.com/"; if (item.TryGetProviderId(MetadataProvider.Imdb, out var externalId)) { - yield return baseUrl + $"title/{externalId}"; + if (item is Person) + { + yield return baseUrl + $"name/{externalId}"; + } + else + { + yield return baseUrl + $"title/{externalId}"; + } } } } From 747fa4699a003db7e574b18e76f6c0b491bf8b21 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 04:59:58 +0000 Subject: [PATCH 089/508] Update actions/setup-dotnet action to v4.3.1 --- .github/workflows/ci-codeql-analysis.yml | 2 +- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/ci-openapi.yml | 4 ++-- .github/workflows/ci-tests.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 850214fec1..4fe073a63a 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -22,7 +22,7 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup .NET - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '9.0.x' diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index ca505790cc..167afb342b 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -17,7 +17,7 @@ jobs: repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup .NET - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '9.0.x' @@ -47,7 +47,7 @@ jobs: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '9.0.x' diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 5a32849877..c3f6b513c7 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -21,7 +21,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup .NET - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '9.0.x' - name: Generate openapi.json @@ -55,7 +55,7 @@ jobs: ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF) git checkout --progress --force $ANCESTOR_REF - name: Setup .NET - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '9.0.x' - name: Generate openapi.json diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index ec78396db0..04c8465e95 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: ${{ env.SDK_VERSION }} From 62fc2b8d0d2a0f4cab13c25a89fc26c78b505caf Mon Sep 17 00:00:00 2001 From: Thunderstrike116 Date: Sun, 16 Mar 2025 09:41:30 +0000 Subject: [PATCH 090/508] Translated using Weblate (Greek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/ --- Emby.Server.Implementations/Localization/Core/el.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 631e659d5c..f3195f0ea0 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -125,7 +125,7 @@ "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο", "External": "Εξωτερικό", "HearingImpaired": "Με προβλήματα ακοής", - "TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay", + "TaskRefreshTrickplayImages": "Δημιουργία εικόνων Trickplay", "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.", "TaskAudioNormalization": "Ομοιομορφία ήχου", "TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.", From e1392ca1b62355d9ce16177b9f69d2bd56c1e0d0 Mon Sep 17 00:00:00 2001 From: Blackspirits Date: Mon, 17 Mar 2025 19:09:51 +0000 Subject: [PATCH 091/508] Translated using Weblate (Portuguese (Portugal)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/ --- Emby.Server.Implementations/Localization/Core/pt-PT.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index 879bf64b0c..42ea5e0a46 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -1,6 +1,6 @@ { "Albums": "Álbuns", - "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}", + "AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}", "Application": "Aplicação", "Artists": "Artistas", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", From 85b5bebda4a887bad03a114e727d9ee5d87961cc Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Tue, 18 Mar 2025 17:37:04 -0600 Subject: [PATCH 092/508] Add fast-path to getting just the SeriesPresentationUniqueKey for NextUp (#13687) * Add more optimized query to calculate series that should be processed for next up * Filter series based on last watched date --- .../Library/LibraryManager.cs | 15 ++ .../TV/TVSeriesManager.cs | 59 ++------ Jellyfin.Api/Controllers/TvShowsController.cs | 4 +- .../Item/BaseItemRepository.cs | 31 +++++ .../Library/ILibraryManager.cs | 9 ++ .../Persistence/IItemRepository.cs | 8 ++ MediaBrowser.Model/Querying/NextUpQuery.cs | 131 +++++++++--------- 7 files changed, 136 insertions(+), 121 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index cc2092e21e..7b3a540398 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1344,6 +1344,21 @@ namespace Emby.Server.Implementations.Library return _itemRepository.GetItemList(query); } + public IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection parents, DateTime dateCutoff) + { + SetTopParentIdsOrAncestors(query, parents); + + if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0) + { + if (query.User is not null) + { + AddUserToQuery(query, query.User); + } + } + + return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff); + } + public QueryResult QueryItems(InternalItemsQuery query) { if (query.User is not null) diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index f8ce473da3..10d27498bf 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV if (!string.IsNullOrEmpty(presentationUniqueKey)) { - return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, options), request); + return GetResult(GetNextUpEpisodes(request, user, [presentationUniqueKey], options), request); } if (limit.HasValue) @@ -99,25 +99,9 @@ namespace Emby.Server.Implementations.TV limit = limit.Value + 10; } - var items = _libraryManager - .GetItemList( - new InternalItemsQuery(user) - { - IncludeItemTypes = new[] { BaseItemKind.Episode }, - OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) }, - SeriesPresentationUniqueKey = presentationUniqueKey, - Limit = limit, - DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false }, - GroupBySeriesPresentationUniqueKey = true - }, - parentsFolders.ToList()) - .Cast() - .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey)) - .Select(GetUniqueSeriesKey) - .ToList(); + var nextUpSeriesKeys = _libraryManager.GetNextUpSeriesKeys(new InternalItemsQuery(user) { Limit = limit }, parentsFolders, request.NextUpDateCutoff); - // Avoid implicitly captured closure - var episodes = GetNextUpEpisodes(request, user, items.Distinct().ToArray(), options); + var episodes = GetNextUpEpisodes(request, user, nextUpSeriesKeys, options); return GetResult(episodes, request); } @@ -133,36 +117,11 @@ namespace Emby.Server.Implementations.TV .OrderByDescending(i => i.LastWatchedDate); } - // If viewing all next up for all series, remove first episodes - // But if that returns empty, keep those first episodes (avoid completely empty view) - var alwaysEnableFirstEpisode = !request.SeriesId.IsNullOrEmpty(); - var anyFound = false; - return allNextUp - .Where(i => - { - if (request.DisableFirstEpisode) - { - return i.LastWatchedDate != DateTime.MinValue; - } - - if (alwaysEnableFirstEpisode || (i.LastWatchedDate != DateTime.MinValue && i.LastWatchedDate.Date >= request.NextUpDateCutoff)) - { - anyFound = true; - return true; - } - - return !anyFound && i.LastWatchedDate == DateTime.MinValue; - }) .Select(i => i.GetEpisodeFunction()) .Where(i => i is not null)!; } - private static string GetUniqueSeriesKey(Episode episode) - { - return episode.SeriesPresentationUniqueKey; - } - private static string GetUniqueSeriesKey(Series series) { return series.GetPresentationUniqueKey(); @@ -178,13 +137,13 @@ namespace Emby.Server.Implementations.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { BaseItemKind.Episode }, + IncludeItemTypes = [BaseItemKind.Episode], IsPlayed = true, Limit = 1, ParentIndexNumberNotEquals = 0, DtoOptions = new DtoOptions { - Fields = new[] { ItemFields.SortName }, + Fields = [ItemFields.SortName], EnableImages = false } }; @@ -202,8 +161,8 @@ namespace Emby.Server.Implementations.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { BaseItemKind.Episode }, - OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) }, + IncludeItemTypes = [BaseItemKind.Episode], + OrderBy = [(ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending)], Limit = 1, IsPlayed = includePlayed, IsVirtualItem = false, @@ -228,7 +187,7 @@ namespace Emby.Server.Implementations.TV AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, ParentIndexNumber = 0, - IncludeItemTypes = new[] { BaseItemKind.Episode }, + IncludeItemTypes = [BaseItemKind.Episode], IsPlayed = includePlayed, IsVirtualItem = false, DtoOptions = dtoOptions @@ -248,7 +207,7 @@ namespace Emby.Server.Implementations.TV consideredEpisodes.Add(nextEpisode); } - var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) }) + var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)]) .Cast(); if (lastWatchedEpisode is not null) { diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index df46c2dac9..cc070244b1 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -86,7 +87,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] bool? enableUserData, [FromQuery] DateTime? nextUpDateCutoff, [FromQuery] bool enableTotalRecordCount = true, - [FromQuery] bool disableFirstEpisode = false, + [FromQuery][ParameterObsolete] bool disableFirstEpisode = false, [FromQuery] bool enableResumable = true, [FromQuery] bool enableRewatching = false) { @@ -109,7 +110,6 @@ public class TvShowsController : BaseJellyfinApiController StartIndex = startIndex, User = user, EnableTotalRecordCount = enableTotalRecordCount, - DisableFirstEpisode = disableFirstEpisode, NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, EnableResumable = enableResumable, EnableRewatching = enableRewatching diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 392b7de74f..e20ad79ad1 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -255,6 +255,37 @@ public sealed class BaseItemRepository return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray(); } + /// + public IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff) + { + ArgumentNullException.ThrowIfNull(filter); + ArgumentNullException.ThrowIfNull(filter.User); + + using var context = _dbProvider.CreateDbContext(); + + var query = context.BaseItems + .AsNoTracking() + .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value)) + .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode]) + .Join( + context.UserData.AsNoTracking(), + i => new { UserId = filter.User.Id, ItemId = i.Id }, + u => new { UserId = u.UserId, ItemId = u.ItemId }, + (entity, data) => new { Item = entity, UserData = data }) + .GroupBy(g => g.Item.SeriesPresentationUniqueKey) + .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) }) + .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff) + .OrderByDescending(g => g.LastPlayedDate) + .Select(g => g.Key!); + + if (filter.Limit.HasValue) + { + query = query.Take(filter.Limit.Value); + } + + return query.ToArray(); + } + private IQueryable ApplyGroupingFilter(IQueryable dbQuery, InternalItemsQuery filter) { // This whole block is needed to filter duplicate entries on request diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 47b1cb16e8..03a28fd8c0 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -565,6 +565,15 @@ namespace MediaBrowser.Controller.Library /// List of items. IReadOnlyList GetItemList(InternalItemsQuery query, List parents); + /// + /// Gets the list of series presentation keys for next up. + /// + /// The query to use. + /// Items to use for query. + /// The minimum date for a series to have been most recently watched. + /// List of series presentation keys. + IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection parents, DateTime dateCutoff); + /// /// Gets the items result. /// diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index afe2d833d5..f1ed4fe274 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -59,6 +59,14 @@ public interface IItemRepository /// List<BaseItem>. IReadOnlyList GetItemList(InternalItemsQuery filter); + /// + /// Gets the list of series presentation keys for next up. + /// + /// The query. + /// The minimum date for a series to have been most recently watched. + /// The list of keys. + IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff); + /// /// Updates the inherited values. /// diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index 8dece28a09..aee720aa7b 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -4,76 +4,69 @@ using System; using Jellyfin.Data.Entities; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Model.Querying +namespace MediaBrowser.Model.Querying; + +public class NextUpQuery { - public class NextUpQuery + public NextUpQuery() { - public NextUpQuery() - { - EnableImageTypes = Array.Empty(); - EnableTotalRecordCount = true; - DisableFirstEpisode = false; - NextUpDateCutoff = DateTime.MinValue; - EnableResumable = false; - EnableRewatching = false; - } - - /// - /// Gets or sets the user. - /// - /// The user. - public required User User { get; set; } - - /// - /// Gets or sets the parent identifier. - /// - /// The parent identifier. - public Guid? ParentId { get; set; } - - /// - /// Gets or sets the series id. - /// - /// The series id. - public Guid? SeriesId { get; set; } - - /// - /// Gets or sets the start index. Use for paging. - /// - /// The start index. - public int? StartIndex { get; set; } - - /// - /// Gets or sets the maximum number of items to return. - /// - /// The limit. - public int? Limit { get; set; } - - /// - /// Gets or sets the enable image types. - /// - /// The enable image types. - public ImageType[] EnableImageTypes { get; set; } - - public bool EnableTotalRecordCount { get; set; } - - /// - /// Gets or sets a value indicating whether do disable sending first episode as next up. - /// - public bool DisableFirstEpisode { get; set; } - - /// - /// Gets or sets a value indicating the oldest date for a show to appear in Next Up. - /// - public DateTime NextUpDateCutoff { get; set; } - - /// - /// Gets or sets a value indicating whether to include resumable episodes as next up. - /// - public bool EnableResumable { get; set; } - - /// - /// Gets or sets a value indicating whether getting rewatching next up list. - /// - public bool EnableRewatching { get; set; } + EnableImageTypes = Array.Empty(); + EnableTotalRecordCount = true; + NextUpDateCutoff = DateTime.MinValue; + EnableResumable = false; + EnableRewatching = false; } + + /// + /// Gets or sets the user. + /// + /// The user. + public required User User { get; set; } + + /// + /// Gets or sets the parent identifier. + /// + /// The parent identifier. + public Guid? ParentId { get; set; } + + /// + /// Gets or sets the series id. + /// + /// The series id. + public Guid? SeriesId { get; set; } + + /// + /// Gets or sets the start index. Use for paging. + /// + /// The start index. + public int? StartIndex { get; set; } + + /// + /// Gets or sets the maximum number of items to return. + /// + /// The limit. + public int? Limit { get; set; } + + /// + /// Gets or sets the enable image types. + /// + /// The enable image types. + public ImageType[] EnableImageTypes { get; set; } + + public bool EnableTotalRecordCount { get; set; } + + /// + /// Gets or sets a value indicating the oldest date for a show to appear in Next Up. + /// + public DateTime NextUpDateCutoff { get; set; } + + /// + /// Gets or sets a value indicating whether to include resumable episodes as next up. + /// + public bool EnableResumable { get; set; } + + /// + /// Gets or sets a value indicating whether getting rewatching next up list. + /// + public bool EnableRewatching { get; set; } } From c24d0c1240d300a4912bb7c6810063b16078927f Mon Sep 17 00:00:00 2001 From: timminator <150205162+timminator@users.noreply.github.com> Date: Wed, 19 Mar 2025 00:40:06 +0100 Subject: [PATCH 093/508] Respect preferred language when selecting forced subtitles (#13098) Rework subtitle selection logic --- .../Library/MediaStreamSelector.cs | 86 +++++++++++++------ 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index ea223e3ece..6791e3ca90 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -39,46 +39,48 @@ namespace Emby.Server.Implementations.Library return null; } + // Sort in the following order: Default > No tag > Forced var sortedStreams = streams .Where(i => i.Type == MediaStreamType.Subtitle) .OrderByDescending(x => x.IsExternal) - .ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(x => x.IsForced) .ThenByDescending(x => x.IsDefault) - .ThenByDescending(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) + .ThenByDescending(x => x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) + .ThenByDescending(x => x.IsForced && IsLanguageUndefined(x.Language)) + .ThenByDescending(x => x.IsForced) .ToList(); MediaStream? stream = null; + if (mode == SubtitlePlaybackMode.Default) { - // Load subtitles according to external, forced and default flags. - stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); + // Load subtitles according to external, default and forced flags. + stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced); } else if (mode == SubtitlePlaybackMode.Smart) { // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages. - // If no subtitles of preferred language available, use default behaviour. + // If no subtitles of preferred language available, use none. + // If the audio language is one of the user's preferred subtitle languages behave like OnlyForced. if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) { - stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ?? - sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); + stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages)); } else { - // Respect forced flag. - stream = sortedStreams.FirstOrDefault(x => x.IsForced); + stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); } } else if (mode == SubtitlePlaybackMode.Always) { - // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour. - stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ?? - sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); + // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour. + stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ?? + BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); } else if (mode == SubtitlePlaybackMode.OnlyForced) { - // Only load subtitles that are flagged forced. - stream = sortedStreams.FirstOrDefault(x => x.IsForced); + // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language + stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); } return stream?.Index; @@ -110,40 +112,72 @@ namespace Emby.Server.Implementations.Library if (mode == SubtitlePlaybackMode.Default) { // Prefer embedded metadata over smart logic - filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault) + // Load subtitles according to external, default, and forced flags. + filteredStreams = sortedStreams.Where(s => s.IsExternal || s.IsDefault || s.IsForced) .ToList(); } else if (mode == SubtitlePlaybackMode.Smart) { // Prefer smart logic over embedded metadata + // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages, otherwise OnlyForced behavior. if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) { - filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)) + filteredStreams = sortedStreams.Where(s => MatchesPreferredLanguage(s.Language, preferredLanguages)) .ToList(); } + else + { + filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages); + } } else if (mode == SubtitlePlaybackMode.Always) { - // Always load the most suitable full subtitles - filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList(); + // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior. + filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages)) + .ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages); } else if (mode == SubtitlePlaybackMode.OnlyForced) { - // Always load the most suitable full subtitles - filteredStreams = sortedStreams.Where(s => s.IsForced).ToList(); + // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language + filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages); } - // Load forced subs if we have found no suitable full subtitles - var iterStreams = filteredStreams is null || filteredStreams.Count == 0 - ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) - : filteredStreams; + // If filteredStreams is null, initialize it as an empty list to avoid null reference errors + filteredStreams ??= new List(); - foreach (var stream in iterStreams) + foreach (var stream in filteredStreams) { stream.Score = GetStreamScore(stream, preferredLanguages); } } + private static bool MatchesPreferredLanguage(string language, IReadOnlyList preferredLanguages) + { + // If preferredLanguages is empty, treat it as "any language" (wildcard) + return preferredLanguages.Count == 0 || + preferredLanguages.Contains(language, StringComparison.OrdinalIgnoreCase); + } + + private static bool IsLanguageUndefined(string language) + { + // Check for null, empty, or known placeholders + return string.IsNullOrEmpty(language) || + language.Equals("und", StringComparison.OrdinalIgnoreCase) || + language.Equals("unknown", StringComparison.OrdinalIgnoreCase) || + language.Equals("undetermined", StringComparison.OrdinalIgnoreCase) || + language.Equals("mul", StringComparison.OrdinalIgnoreCase) || + language.Equals("zxx", StringComparison.OrdinalIgnoreCase); + } + + private static List BehaviorOnlyForced(IEnumerable sortedStreams, IReadOnlyList preferredLanguages) + { + return sortedStreams + .Where(s => s.IsForced && (MatchesPreferredLanguage(s.Language, preferredLanguages) || IsLanguageUndefined(s.Language))) + .OrderByDescending(s => MatchesPreferredLanguage(s.Language, preferredLanguages)) + .ThenByDescending(s => IsLanguageUndefined(s.Language)) + .ToList(); + } + internal static int GetStreamScore(MediaStream stream, IReadOnlyList languagePreferences) { var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase)); From 11fbca45ff8f417108d5655b7c0a4971e25df4c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:41:02 -0600 Subject: [PATCH 094/508] Update actions/download-artifact action to v4.2.0 (#13734) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/ci-openapi.yml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 167afb342b..26299c202e 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -85,13 +85,13 @@ jobs: steps: - name: Download abi-head - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 with: name: abi-head path: abi-head - name: Download abi-base - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 with: name: abi-base path: abi-base diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index c3f6b513c7..b57be931c5 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -80,12 +80,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 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@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 with: name: openapi-head path: openapi-head @@ -220,7 +220,7 @@ jobs: run: |- echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 with: name: openapi-head path: openapi-head From 3eca221cc6ea7af624153ff27a989aec37cb1cfb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 18:27:37 +0000 Subject: [PATCH 095/508] Update CI dependencies --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- .github/workflows/ci-compat.yml | 8 ++++---- .github/workflows/ci-openapi.yml | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 4fe073a63a..6a3d4d3514 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 + uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 + uses: github/codeql-action/autobuild@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 + uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 26299c202e..13b029e52c 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -26,7 +26,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 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@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: abi-base retention-days: 14 @@ -85,13 +85,13 @@ jobs: steps: - name: Download abi-head - uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: abi-head path: abi-head - name: Download abi-base - uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: abi-base path: abi-base diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index b57be931c5..95e090f9b6 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.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@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 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@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: openapi-base retention-days: 14 @@ -80,12 +80,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 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@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: openapi-head path: openapi-head @@ -220,7 +220,7 @@ jobs: run: |- echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: openapi-head path: openapi-head From aabaf1a656e732e5208e251271bd1553c2b576de Mon Sep 17 00:00:00 2001 From: Lampan-git Date: Thu, 20 Mar 2025 05:55:51 -0400 Subject: [PATCH 096/508] Backport pull request #13720 from jellyfin/release-10.10.z Fix regression where "Search for missing metadata" not handling cast having multiple roles Original-merge: 91ca81eca7d2c984a096a396cbd83d0111f41c9d Merged-by: Bond-009 Backported-by: Bond_009 --- .../Manager/MetadataService.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 1d3ddc4e24..e8994693de 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -1146,13 +1146,24 @@ namespace MediaBrowser.Providers.Manager private static void MergePeople(IReadOnlyList source, IReadOnlyList target) { - foreach (var person in target) - { - var normalizedName = person.Name.RemoveDiacritics(); - var personInSource = source.FirstOrDefault(i => string.Equals(i.Name.RemoveDiacritics(), normalizedName, StringComparison.OrdinalIgnoreCase)); + var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase); + var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase); - if (personInSource is not null) + foreach (var name in targetByName.Select(g => g.Key)) + { + var targetPeople = targetByName[name].ToArray(); + var sourcePeople = sourceByName[name].ToArray(); + + if (sourcePeople.Length == 0) { + continue; + } + + for (int i = 0; i < targetPeople.Length; i++) + { + var person = targetPeople[i]; + var personInSource = i < sourcePeople.Length ? sourcePeople[i] : sourcePeople[0]; + foreach (var providerId in personInSource.ProviderIds) { person.ProviderIds.TryAdd(providerId.Key, providerId.Value); From 350983e03cc354e083cccdd156d3672cc7125685 Mon Sep 17 00:00:00 2001 From: timminator <150205162+timminator@users.noreply.github.com> Date: Thu, 20 Mar 2025 14:10:48 +0100 Subject: [PATCH 097/508] Fix OnPlaybackStopped task erroring out (#13226) --- .../Library/MediaSourceManager.cs | 10 +++++++--- .../Session/SessionManager.cs | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 5795c47ccc..92a5e9ffd9 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -782,9 +782,13 @@ namespace Emby.Server.Implementations.Library { ArgumentException.ThrowIfNullOrEmpty(id); - // TODO probably shouldn't throw here but it is kept for "backwards compatibility" - var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException(); - return Task.FromResult(new Tuple(info.MediaSource, info as IDirectStreamProvider)); + var info = GetLiveStreamInfo(id); + if (info is null) + { + return Task.FromResult>(new Tuple(null, null)); + } + + return Task.FromResult>(new Tuple(info.MediaSource, info as IDirectStreamProvider)); } public ILiveStream GetLiveStreamInfo(string id) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 030da6f73e..df2acfc46c 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -343,6 +343,11 @@ namespace Emby.Server.Implementations.Session /// Task. private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bool updateLastCheckInTime) { + if (session is null) + { + return; + } + if (string.IsNullOrEmpty(info.MediaSourceId)) { info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); @@ -675,6 +680,11 @@ namespace Emby.Server.Implementations.Session private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId) { + if (session is null) + { + return null; + } + var item = session.FullNowPlayingItem; if (item is not null && item.Id.Equals(itemId)) { @@ -794,7 +804,11 @@ namespace Emby.Server.Implementations.Session ArgumentNullException.ThrowIfNull(info); - var session = GetSession(info.SessionId); + var session = GetSession(info.SessionId, false); + if (session is null) + { + return; + } var libraryItem = info.ItemId.IsEmpty() ? null From c77a0719c28ec6744c0618c18e93f2b36c7d95f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Sun, 23 Mar 2025 01:30:32 +0100 Subject: [PATCH 098/508] Clear dictionaries when not needed, use set for finding existing base items (#13749) --- .../Migrations/Routines/MigrateLibraryDb.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index dfe497c490..d2fbcbec94 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -163,7 +163,6 @@ public class MigrateLibraryDb : IMigrationRoutine dbContext.UserData.ExecuteDelete(); var users = dbContext.Users.AsNoTracking().ToImmutableArray(); - var oldUserdata = new Dictionary(); foreach (var entity in queryResult) { @@ -184,6 +183,8 @@ public class MigrateLibraryDb : IMigrationRoutine dbContext.UserData.Add(userData); } + users.Clear(); + legacyBaseItemWithUserKeys.Clear(); _logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count); dbContext.SaveChanges(); @@ -220,11 +221,12 @@ public class MigrateLibraryDb : IMigrationRoutine dbContext.PeopleBaseItemMap.ExecuteDelete(); var peopleCache = new Dictionary Items)>(); + var baseItemIds = dbContext.BaseItems.Select(b => b.Id).ToHashSet(); foreach (SqliteDataReader reader in connection.Query(personsQuery)) { var itemId = reader.GetGuid(0); - if (!dbContext.BaseItems.Any(f => f.Id == itemId)) + if (!baseItemIds.Contains(itemId)) { _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1)); continue; @@ -256,12 +258,16 @@ public class MigrateLibraryDb : IMigrationRoutine }); } + baseItemIds.Clear(); + foreach (var item in peopleCache) { dbContext.Peoples.Add(item.Value.Person); dbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId))); } + peopleCache.Clear(); + _logger.LogInformation("Try saving {0} People entries.", dbContext.MediaStreamInfos.Local.Count); dbContext.SaveChanges(); migrationTotalTime += stopwatch.Elapsed; From 8b6aec7ce54df949b2940daa7f0c6b7d3201bda5 Mon Sep 17 00:00:00 2001 From: Adil Date: Sun, 23 Mar 2025 19:31:26 +0500 Subject: [PATCH 099/508] Rename Pakistan to select dropdown accessible name (#13752) --- Emby.Server.Implementations/Localization/countries.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json index 0a11b3e458..d92dc880b1 100644 --- a/Emby.Server.Implementations/Localization/countries.json +++ b/Emby.Server.Implementations/Localization/countries.json @@ -336,7 +336,7 @@ "TwoLetterISORegionName": "IE" }, { - "DisplayName": "Islamic Republic of Pakistan", + "DisplayName": "Pakistan", "Name": "PK", "ThreeLetterISORegionName": "PAK", "TwoLetterISORegionName": "PK" From 8db6a39e92acfd76689e77c71b00ac96e60c515b Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sun, 23 Mar 2025 17:05:13 +0100 Subject: [PATCH 100/508] Remove all DB data on item removal, delete internal trickplay files (#13753) --- .../Data/CleanDatabaseScheduledTask.cs | 114 +++++++++--------- .../Library/LibraryManager.cs | 56 ++++++--- .../Library/PathManager.cs | 36 ++++++ .../Item/BaseItemRepository.cs | 23 ++-- .../Trickplay/TrickplayManager.cs | 16 +-- MediaBrowser.Controller/IO/IPathManager.cs | 17 +++ 6 files changed, 170 insertions(+), 92 deletions(-) create mode 100644 Emby.Server.Implementations/Library/PathManager.cs create mode 100644 MediaBrowser.Controller/IO/IPathManager.cs diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 7ea863d769..a83ded439c 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -5,80 +5,80 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Server.Implementations; +using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Trickplay; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Data +namespace Emby.Server.Implementations.Data; + +public class CleanDatabaseScheduledTask : ILibraryPostScanTask { - public class CleanDatabaseScheduledTask : ILibraryPostScanTask + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IDbContextFactory _dbProvider; + + public CleanDatabaseScheduledTask( + ILibraryManager libraryManager, + ILogger logger, + IDbContextFactory dbProvider) { - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IDbContextFactory _dbProvider; + _libraryManager = libraryManager; + _logger = logger; + _dbProvider = dbProvider; + } - public CleanDatabaseScheduledTask( - ILibraryManager libraryManager, - ILogger logger, - IDbContextFactory dbProvider) - { - _libraryManager = libraryManager; - _logger = logger; - _dbProvider = dbProvider; - } + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false); + } - public async Task Run(IProgress progress, CancellationToken cancellationToken) + private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) + { + var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery { - await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false); - } + HasDeadParentId = true + }); - private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) + var numComplete = 0; + var numItems = itemIds.Count + 1; + + _logger.LogDebug("Cleaning {Number} items with dead parent links", numItems); + + foreach (var itemId in itemIds) { - var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery + cancellationToken.ThrowIfCancellationRequested(); + + var item = _libraryManager.GetItemById(itemId); + if (item is not null) { - HasDeadParentId = true - }); + _logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty); - var numComplete = 0; - var numItems = itemIds.Count + 1; - - _logger.LogDebug("Cleaning {0} items with dead parent links", numItems); - - foreach (var itemId in itemIds) - { - cancellationToken.ThrowIfCancellationRequested(); - - var item = _libraryManager.GetItemById(itemId); - - if (item is not null) + _libraryManager.DeleteItem(item, new DeleteOptions { - _logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty); - - _libraryManager.DeleteItem(item, new DeleteOptions - { - DeleteFileLocation = false - }); - } - - numComplete++; - double percent = numComplete; - percent /= numItems; - progress.Report(percent * 100); + DeleteFileLocation = false + }); } - var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - await using (context.ConfigureAwait(false)) - { - var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); - await using (transaction.ConfigureAwait(false)) - { - await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); - } - } - - progress.Report(100); + numComplete++; + double percent = numComplete; + percent /= numItems; + progress.Report(percent * 100); } + + var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) + { + var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await using (transaction.ConfigureAwait(false)) + { + await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + } + } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 7b3a540398..3432aa3222 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -78,6 +78,7 @@ namespace Emby.Server.Implementations.Library private readonly NamingOptions _namingOptions; private readonly IPeopleRepository _peopleRepository; private readonly ExtraResolver _extraResolver; + private readonly IPathManager _pathManager; /// /// The _root folder sync lock. @@ -113,7 +114,8 @@ namespace Emby.Server.Implementations.Library /// The image processor. /// The naming options. /// The directory service. - /// The People Repository. + /// The people repository. + /// The path manager. public LibraryManager( IServerApplicationHost appHost, ILoggerFactory loggerFactory, @@ -130,7 +132,8 @@ namespace Emby.Server.Implementations.Library IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService, - IPeopleRepository peopleRepository) + IPeopleRepository peopleRepository, + IPathManager pathManager) { _appHost = appHost; _logger = loggerFactory.CreateLogger(); @@ -148,6 +151,7 @@ namespace Emby.Server.Implementations.Library _cache = new ConcurrentDictionary(); _namingOptions = namingOptions; _peopleRepository = peopleRepository; + _pathManager = pathManager; _extraResolver = new ExtraResolver(loggerFactory.CreateLogger(), namingOptions, directoryService); _configurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -200,33 +204,33 @@ namespace Emby.Server.Implementations.Library /// Gets or sets the postscan tasks. /// /// The postscan tasks. - private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty(); + private ILibraryPostScanTask[] PostscanTasks { get; set; } = []; /// /// Gets or sets the intro providers. /// /// The intro providers. - private IIntroProvider[] IntroProviders { get; set; } = Array.Empty(); + private IIntroProvider[] IntroProviders { get; set; } = []; /// /// Gets or sets the list of entity resolution ignore rules. /// /// The entity resolution ignore rules. - private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty(); + private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = []; /// /// Gets or sets the list of currently registered entity resolvers. /// /// The entity resolvers enumerable. - private IItemResolver[] EntityResolvers { get; set; } = Array.Empty(); + private IItemResolver[] EntityResolvers { get; set; } = []; - private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty(); + private IMultiItemResolver[] MultiItemResolvers { get; set; } = []; /// /// Gets or sets the comparers. /// /// The comparers. - private IBaseItemComparer[] Comparers { get; set; } = Array.Empty(); + private IBaseItemComparer[] Comparers { get; set; } = []; public bool IsScanRunning { get; private set; } @@ -359,7 +363,7 @@ namespace Emby.Server.Implementations.Library var children = item.IsFolder ? ((Folder)item).GetRecursiveChildren(false) - : Array.Empty(); + : []; foreach (var metadataPath in GetMetadataPaths(item, children)) { @@ -465,14 +469,28 @@ namespace Emby.Server.Implementations.Library ReportItemRemoved(item, parent); } - private static List GetMetadataPaths(BaseItem item, IEnumerable children) + private List GetMetadataPaths(BaseItem item, IEnumerable children) + { + var list = GetInternalMetadataPaths(item); + foreach (var child in children) + { + list.AddRange(GetInternalMetadataPaths(child)); + } + + return list; + } + + private List GetInternalMetadataPaths(BaseItem item) { var list = new List { item.GetInternalMetadataPath() }; - list.AddRange(children.Select(i => i.GetInternalMetadataPath())); + if (item is Video video) + { + list.Add(_pathManager.GetTrickplayDirectory(video)); + } return list; } @@ -593,7 +611,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf); - files = Array.Empty(); + files = []; } else { @@ -1463,7 +1481,7 @@ namespace Emby.Server.Implementations.Library // Optimize by querying against top level views query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); - query.AncestorIds = Array.Empty(); + query.AncestorIds = []; // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) @@ -1583,7 +1601,7 @@ namespace Emby.Server.Implementations.Library return GetTopParentIdsForQuery(displayParent, user); } - return Array.Empty(); + return []; } if (!view.ParentId.IsEmpty()) @@ -1594,7 +1612,7 @@ namespace Emby.Server.Implementations.Library return GetTopParentIdsForQuery(displayParent, user); } - return Array.Empty(); + return []; } // Handle grouping @@ -1609,7 +1627,7 @@ namespace Emby.Server.Implementations.Library .SelectMany(i => GetTopParentIdsForQuery(i, user)); } - return Array.Empty(); + return []; } if (item is CollectionFolder collectionFolder) @@ -1623,7 +1641,7 @@ namespace Emby.Server.Implementations.Library return new[] { topParent.Id }; } - return Array.Empty(); + return []; } /// @@ -1667,7 +1685,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Error getting intros"); - return Enumerable.Empty(); + return []; } } @@ -2894,7 +2912,7 @@ namespace Emby.Server.Implementations.Library { var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values? - await File.WriteAllBytesAsync(path, Array.Empty()).ConfigureAwait(false); + await File.WriteAllBytesAsync(path, []).ConfigureAwait(false); } CollectionFolder.SaveLibraryOptions(virtualFolderPath, options); diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs new file mode 100644 index 0000000000..c910abadbc --- /dev/null +++ b/Emby.Server.Implementations/Library/PathManager.cs @@ -0,0 +1,36 @@ +using System.Globalization; +using System.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; + +namespace Emby.Server.Implementations.Library; + +/// +/// IPathManager implementation. +/// +public class PathManager : IPathManager +{ + private readonly IServerConfigurationManager _config; + + /// + /// Initializes a new instance of the class. + /// + /// The server configuration manager. + public PathManager( + IServerConfigurationManager config) + { + _config = config; + } + + /// + public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false) + { + var basePath = _config.ApplicationPaths.TrickplayPath; + var idString = item.Id.ToString("N", CultureInfo.InvariantCulture); + + return saveWithMedia + ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) + : Path.Combine(basePath, idString); + } +} diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index e20ad79ad1..630a169cba 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -101,16 +101,23 @@ public sealed class BaseItemRepository using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); + context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete(); + context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItems.Where(e => e.Id == id).ExecuteDelete(); + context.Chapters.Where(e => e.ItemId == id).ExecuteDelete(); + context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete(); + context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete(); + context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete(); + context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete(); + context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete(); + context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete(); context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete(); - context.Chapters.Where(e => e.ItemId == id).ExecuteDelete(); - context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); - context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete(); - context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete(); - context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete(); - context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete(); - context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete(); - context.BaseItems.Where(e => e.Id == id).ExecuteDelete(); + context.TrickplayInfos.Where(e => e.ItemId == id).ExecuteDelete(); context.SaveChanges(); transaction.Commit(); } diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 9c0f5b57b4..6949ec1a8c 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -12,6 +12,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Trickplay; @@ -37,9 +38,10 @@ public class TrickplayManager : ITrickplayManager private readonly IImageEncoder _imageEncoder; private readonly IDbContextFactory _dbProvider; private readonly IApplicationPaths _appPaths; + private readonly IPathManager _pathManager; private static readonly AsyncNonKeyedLocker _resourcePool = new(1); - private static readonly string[] _trickplayImgExtensions = { ".jpg" }; + private static readonly string[] _trickplayImgExtensions = [".jpg"]; /// /// Initializes a new instance of the class. @@ -53,6 +55,7 @@ public class TrickplayManager : ITrickplayManager /// The image encoder. /// The database provider. /// The application paths. + /// The path manager. public TrickplayManager( ILogger logger, IMediaEncoder mediaEncoder, @@ -62,7 +65,8 @@ public class TrickplayManager : ITrickplayManager IServerConfigurationManager config, IImageEncoder imageEncoder, IDbContextFactory dbProvider, - IApplicationPaths appPaths) + IApplicationPaths appPaths, + IPathManager pathManager) { _logger = logger; _mediaEncoder = mediaEncoder; @@ -73,6 +77,7 @@ public class TrickplayManager : ITrickplayManager _imageEncoder = imageEncoder; _dbProvider = dbProvider; _appPaths = appPaths; + _pathManager = pathManager; } /// @@ -610,12 +615,7 @@ public class TrickplayManager : ITrickplayManager /// public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false) { - var basePath = _config.ApplicationPaths.TrickplayPath; - var idString = item.Id.ToString("N", CultureInfo.InvariantCulture); - var path = saveWithMedia - ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) - : Path.Combine(basePath, idString); - + var path = _pathManager.GetTrickplayDirectory(item, saveWithMedia); var subdirectory = string.Format( CultureInfo.InvariantCulture, "{0} - {1}x{2}", diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs new file mode 100644 index 0000000000..0368898102 --- /dev/null +++ b/MediaBrowser.Controller/IO/IPathManager.cs @@ -0,0 +1,17 @@ +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.IO; + +/// +/// Interface ITrickplayManager. +/// +public interface IPathManager +{ + /// + /// Gets the path to the trickplay image base folder. + /// + /// The item. + /// Whether or not the tile should be saved next to the media file. + /// The absolute path. + public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false); +} From dfb485d1f205c8eda95bc3b79d341f3c3aef7ec4 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sun, 23 Mar 2025 17:05:40 +0100 Subject: [PATCH 101/508] Rework season folder parsing (#11748) --- Emby.Naming/TV/SeasonPathParser.cs | 108 ++++++++---------- .../Library/LibraryManager.cs | 7 +- .../Library/Resolvers/TV/SeasonResolver.cs | 2 +- .../Library/Resolvers/TV/SeriesResolver.cs | 7 +- MediaBrowser.Controller/Entities/TV/Season.cs | 2 +- .../Library/ILibraryManager.cs | 3 +- .../TV/SeasonPathParserTests.cs | 68 +++++++---- 7 files changed, 105 insertions(+), 92 deletions(-) diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index 45b91971bf..98ee1e4b8f 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -1,43 +1,35 @@ using System; using System.Globalization; using System.IO; +using System.Text.RegularExpressions; namespace Emby.Naming.TV { /// /// Class to parse season paths. /// - public static class SeasonPathParser + public static partial class SeasonPathParser { - /// - /// A season folder must contain one of these somewhere in the name. - /// - private static readonly string[] _seasonFolderNames = - { - "season", - "sæson", - "temporada", - "saison", - "staffel", - "series", - "сезон", - "stagione" - }; + [GeneratedRegex(@"^\s*((?(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?.*)$")] + private static partial Regex ProcessPre(); - private static readonly char[] _splitChars = ['.', '_', ' ', '-']; + [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?(?>\d+)(?!\s*[Ee]\d+))(?.*)$")] + private static partial Regex ProcessPost(); /// /// Attempts to parse season number from path. /// /// Path to season. + /// Folder name of the parent. /// Support special aliases when parsing. /// Support numeric season folders when parsing. /// Returns object. - public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders) + public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool supportNumericSeasonFolders) { var result = new SeasonPathParserResult(); + var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name; - var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders); + var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, supportNumericSeasonFolders); result.SeasonNumber = seasonNumber; @@ -54,15 +46,24 @@ namespace Emby.Naming.TV /// Gets the season number from path. /// /// The path. + /// The parent folder name. /// if set to true [support special aliases]. /// if set to true [support numeric season folders]. /// System.Nullable{System.Int32}. private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath( string path, + string? parentFolderName, bool supportSpecialAliases, bool supportNumericSeasonFolders) { string filename = Path.GetFileName(path); + filename = Regex.Replace(filename, "[ ._-]", string.Empty); + + if (parentFolderName is not null) + { + parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty); + filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase); + } if (supportSpecialAliases) { @@ -85,53 +86,38 @@ namespace Emby.Naming.TV } } - if (TryGetSeasonNumberFromPart(filename, out int seasonNumber)) + if (filename.StartsWith('s')) { + var testFilename = filename.AsSpan()[1..]; + + if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) + { + return (val, true); + } + } + + var preMatch = ProcessPre().Match(filename); + if (preMatch.Success) + { + return CheckMatch(preMatch); + } + else + { + var postMatch = ProcessPost().Match(filename); + return CheckMatch(postMatch); + } + } + + private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match) + { + var numberString = match.Groups["seasonnumber"]; + if (numberString.Success) + { + var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture); return (seasonNumber, true); } - // Look for one of the season folder names - foreach (var name in _seasonFolderNames) - { - if (filename.Contains(name, StringComparison.OrdinalIgnoreCase)) - { - var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase)); - if (result.SeasonNumber.HasValue) - { - return result; - } - - break; - } - } - - var parts = filename.Split(_splitChars, StringSplitOptions.RemoveEmptyEntries); - foreach (var part in parts) - { - if (TryGetSeasonNumberFromPart(part, out seasonNumber)) - { - return (seasonNumber, true); - } - } - - return (null, true); - } - - private static bool TryGetSeasonNumberFromPart(ReadOnlySpan part, out int seasonNumber) - { - seasonNumber = 0; - if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - seasonNumber = value; - return true; - } - - return false; + return (null, false); } /// diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 3432aa3222..b810ad4de1 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2512,8 +2512,11 @@ namespace Emby.Server.Implementations.Library } /// - public int? GetSeasonNumberFromPath(string path) - => SeasonPathParser.Parse(path, true, true).SeasonNumber; + public int? GetSeasonNumberFromPath(string path, Guid? parentId) + { + var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null; + return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber; + } /// public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index abf2d01159..6cb63a28a2 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var path = args.Path; - var seasonParserResult = SeasonPathParser.Parse(path, true, true); + var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true); var season = new Season { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index fb48d7bf17..c81a0adb89 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { if (child.IsDirectory) { - if (IsSeasonFolder(child.FullName, isTvContentType)) + if (IsSeasonFolder(child.FullName, path, isTvContentType)) { _logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName); return true; @@ -155,11 +155,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// Determines whether [is season folder] [the specified path]. /// /// The path. + /// The parentpath. /// if set to true [is tv content type]. /// true if [is season folder] [the specified path]; otherwise, false. - private static bool IsSeasonFolder(string path, bool isTvContentType) + private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType) { - var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber; + var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber; return seasonNumber.HasValue; } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index e3fbe8e4d6..9dbac1e920 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -257,7 +257,7 @@ namespace MediaBrowser.Controller.Entities.TV if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path)) { - IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path); + IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path, ParentId); // If a change was made record it if (IndexNumber.HasValue) diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 03a28fd8c0..e4490bca3b 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -426,8 +426,9 @@ namespace MediaBrowser.Controller.Library /// Gets the season number from path. /// /// The path. + /// The parent id. /// System.Nullable<System.Int32>. - int? GetSeasonNumberFromPath(string path); + int? GetSeasonNumberFromPath(string path, Guid? parentId); /// /// Fills the missing episode numbers from path. diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs index 3a042df683..4c8ba58d04 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs @@ -6,32 +6,54 @@ namespace Jellyfin.Naming.Tests.TV; public class SeasonPathParserTests { [Theory] - [InlineData("/Drive/Season 1", 1, true)] - [InlineData("/Drive/s1", 1, true)] - [InlineData("/Drive/S1", 1, true)] - [InlineData("/Drive/Season 2", 2, true)] - [InlineData("/Drive/Season 02", 2, true)] - [InlineData("/Drive/Seinfeld/S02", 2, true)] - [InlineData("/Drive/Seinfeld/2", 2, true)] - [InlineData("/Drive/Seinfeld - S02", 2, true)] - [InlineData("/Drive/Season 2009", 2009, true)] - [InlineData("/Drive/Season1", 1, true)] - [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)] - [InlineData("/Drive/Season 7 (2016)", 7, false)] - [InlineData("/Drive/Staffel 7 (2016)", 7, false)] - [InlineData("/Drive/Stagione 7 (2016)", 7, false)] - [InlineData("/Drive/Season (8)", null, false)] - [InlineData("/Drive/3.Staffel", 3, false)] - [InlineData("/Drive/s06e05", null, false)] - [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)] - [InlineData("/Drive/extras", 0, true)] - [InlineData("/Drive/specials", 0, true)] - public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory) + [InlineData("/Drive/Season 1", "/Drive", 1, true)] + [InlineData("/Drive/Staffel 1", "/Drive", 1, true)] + [InlineData("/Drive/Stagione 1", "/Drive", 1, true)] + [InlineData("/Drive/sæson 1", "/Drive", 1, true)] + [InlineData("/Drive/Temporada 1", "/Drive", 1, true)] + [InlineData("/Drive/series 1", "/Drive", 1, true)] + [InlineData("/Drive/Kausi 1", "/Drive", 1, true)] + [InlineData("/Drive/Säsong 1", "/Drive", 1, true)] + [InlineData("/Drive/Seizoen 1", "/Drive", 1, true)] + [InlineData("/Drive/Seasong 1", "/Drive", 1, true)] + [InlineData("/Drive/Sezon 1", "/Drive", 1, true)] + [InlineData("/Drive/sezona 1", "/Drive", 1, true)] + [InlineData("/Drive/sezóna 1", "/Drive", 1, true)] + [InlineData("/Drive/Sezonul 1", "/Drive", 1, true)] + [InlineData("/Drive/시즌 1", "/Drive", 1, true)] + [InlineData("/Drive/シーズン 1", "/Drive", 1, true)] + [InlineData("/Drive/сезон 1", "/Drive", 1, true)] + [InlineData("/Drive/Сезон 1", "/Drive", 1, true)] + [InlineData("/Drive/Season 10", "/Drive", 10, true)] + [InlineData("/Drive/Season 100", "/Drive", 100, true)] + [InlineData("/Drive/s1", "/Drive", 1, true)] + [InlineData("/Drive/S1", "/Drive", 1, true)] + [InlineData("/Drive/Season 2", "/Drive", 2, true)] + [InlineData("/Drive/Season 02", "/Drive", 2, true)] + [InlineData("/Drive/Seinfeld/S02", "/Seinfeld", 2, true)] + [InlineData("/Drive/Seinfeld/2", "/Seinfeld", 2, true)] + [InlineData("/Drive/Seinfeld Season 2", "/Drive", null, false)] + [InlineData("/Drive/Season 2009", "/Drive", 2009, true)] + [InlineData("/Drive/Season1", "/Drive", 1, true)] + [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", "/The Wonder Years", 4, true)] + [InlineData("/Drive/Season 7 (2016)", "/Drive", 7, true)] + [InlineData("/Drive/Staffel 7 (2016)", "/Drive", 7, true)] + [InlineData("/Drive/Stagione 7 (2016)", "/Drive", 7, true)] + [InlineData("/Drive/Stargate SG-1/Season 1", "/Drive/Stargate SG-1", 1, true)] + [InlineData("/Drive/Stargate SG-1/Stargate SG-1 Season 1", "/Drive/Stargate SG-1", 1, true)] + [InlineData("/Drive/Season (8)", "/Drive", null, false)] + [InlineData("/Drive/3.Staffel", "/Drive", 3, true)] + [InlineData("/Drive/s06e05", "/Drive", null, false)] + [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", "/Drive", null, false)] + [InlineData("/Drive/extras", "/Drive", 0, true)] + [InlineData("/Drive/specials", "/Drive", 0, true)] + [InlineData("/Drive/Episode 1 Season 2", "/Drive", null, false)] + public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory) { - var result = SeasonPathParser.Parse(path, true, true); + var result = SeasonPathParser.Parse(path, parentPath, true, true); Assert.Equal(result.SeasonNumber is not null, result.Success); - Assert.Equal(result.SeasonNumber, seasonNumber); + Assert.Equal(seasonNumber, result.SeasonNumber); Assert.Equal(isSeasonDirectory, result.IsSeasonFolder); } } From b3b2da681f8f599391ff318ce0b52c7f022c9dc4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 20:36:11 +0000 Subject: [PATCH 102/508] Update danielpalme/ReportGenerator-GitHub-Action action to v5.4.5 --- .github/workflows/ci-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 04c8465e95..be4192a446 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -35,7 +35,7 @@ jobs: --verbosity minimal - name: Merge code coverage results - uses: danielpalme/ReportGenerator-GitHub-Action@f1927db1dbfc029b056583ee488832e939447fe6 # v5.4.4 + uses: danielpalme/ReportGenerator-GitHub-Action@25b1e0261a9f68d7874dbbace168300558ef68f7 # v5.4.5 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" From ea6130b354d263777b303ff9d79cab9aa4f22fb7 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 23 Mar 2025 21:55:26 +0100 Subject: [PATCH 103/508] Add missing singleton --- Emby.Server.Implementations/ApplicationHost.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 29967c6df5..4fe1d2b17e 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -57,6 +57,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Lyrics; @@ -508,6 +509,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); // TODO: Refactor to eliminate the circular dependencies here so that Lazy isn't required serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); From 671d801d9f734665d0acbd441246712ad2e3d91f Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 24 Mar 2025 02:52:34 +0100 Subject: [PATCH 104/508] #13540 Fixed (#13757) #13508 Partially fixed Co-authored-by: JPVenson --- .../Item/BaseItemRepository.cs | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 630a169cba..bea69b2820 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -963,25 +963,11 @@ public sealed class BaseItemRepository using var context = _dbProvider.CreateDbContext(); - var innerQuery = new InternalItemsQuery(filter.User) - { - ExcludeItemTypes = filter.ExcludeItemTypes, - IncludeItemTypes = filter.IncludeItemTypes, - MediaTypes = filter.MediaTypes, - AncestorIds = filter.AncestorIds, - ItemIds = filter.ItemIds, - TopParentIds = filter.TopParentIds, - ParentId = filter.ParentId, - IsAiring = filter.IsAiring, - IsMovie = filter.IsMovie, - IsSports = filter.IsSports, - IsKids = filter.IsKids, - IsNews = filter.IsNews, - IsSeries = filter.IsSeries - }; - var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, innerQuery); + var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter); - query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type))); + query = query.Where(e => e.Type == returnType); + // this does not seem to be nesseary but it does not make any sense why this isn't working. + // && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type))); if (filter.OrderBy.Count != 0 || !string.IsNullOrEmpty(filter.SearchTerm)) From 3c2d3ac18b6c4c29c340ea8bb3f1dcafe557904b Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 24 Mar 2025 09:19:00 +0100 Subject: [PATCH 105/508] Update src/Jellyfin.Database/readme.md Co-authored-by: Tim Eisele --- src/Jellyfin.Database/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jellyfin.Database/readme.md b/src/Jellyfin.Database/readme.md index dc1479897d..d50636b779 100644 --- a/src/Jellyfin.Database/readme.md +++ b/src/Jellyfin.Database/readme.md @@ -2,7 +2,7 @@ This shall provide context on how to work with entity frameworks multi provider migration feature. -Jellyfin will in the future support multiple database providers, namely SqLite as its default and the experimental postgresSQL. +Jellyfin will support multiple database providers in the future, namely SqLite as its default and the experimental postgresSQL. Each provider has its own set of migrations, as they contain provider specific instructions to migrate the specific changes to their respective systems. From aa4936c59c3307dd14af193933a44d33f738c9ce Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 24 Mar 2025 08:27:21 +0000 Subject: [PATCH 106/508] Added OpenAPI spec for #12880 --- .../ApiServiceCollectionExtensions.cs | 1 + .../RetryOnTemporarlyUnavailableFilter.cs | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 597643ed19..1a327e0f9f 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -247,6 +247,7 @@ namespace Jellyfin.Server.Extensions c.AddSwaggerTypeMappings(); c.SchemaFilter(); + c.OperationFilter(); c.OperationFilter(); c.OperationFilter(); c.OperationFilter(); diff --git a/Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs b/Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs new file mode 100644 index 0000000000..bd572df9b7 --- /dev/null +++ b/Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Net.Http.Headers; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters; + +internal class RetryOnTemporarlyUnavailableFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + operation.Responses.Add("503", new OpenApiResponse() + { + Description = "The server is currently starting or is temporarly not available.", + Headers = new Dictionary() + { + { + "retry-after", + new() { AllowEmptyValue = true, Required = false, Description = "A hint for when to retry the operation." } + }, + { + "message", + new() { AllowEmptyValue = true, Required = false, Description = "A short plain-text reason why the server is not available." } + } + }, + Content = new Dictionary() + { + { + "text/html", + new OpenApiMediaType() + } + } + }); + } +} From a026a3722c5ee6505b6361cee1da073393c21717 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 24 Mar 2025 08:31:52 +0000 Subject: [PATCH 107/508] Clarified retry value type --- Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs b/Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs index bd572df9b7..584519441a 100644 --- a/Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs +++ b/Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs @@ -17,7 +17,7 @@ internal class RetryOnTemporarlyUnavailableFilter : IOperationFilter { { "retry-after", - new() { AllowEmptyValue = true, Required = false, Description = "A hint for when to retry the operation." } + new() { AllowEmptyValue = true, Required = false, Description = "A hint for when to retry the operation in full secounds." } }, { "message", From e4b11c664c79b5ab11ca8487b171d0aacc4343b2 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 24 Mar 2025 08:38:17 +0000 Subject: [PATCH 108/508] Disabled flaky tests --- .../Controllers/UserLibraryControllerTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs index 8df86111ee..98ad28f5bd 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs @@ -75,7 +75,7 @@ public sealed class UserLibraryControllerTests : IClassFixture Date: Mon, 24 Mar 2025 10:07:52 +0000 Subject: [PATCH 109/508] renamed SqLite to Sqlite --- .vscode/settings.json | 3 +++ .../Extensions/ServiceCollectionExtensions.cs | 4 ++-- .../Jellyfin.Server.Implementations.csproj | 1 - Jellyfin.Server/Program.cs | 2 +- Jellyfin.sln | 2 +- .../DoNotUseReturningClauseConvention.cs | 0 .../Jellyfin.Database.Providers.Sqlite.csproj} | 0 .../Migrations/.gitattributes | 0 .../20200514181226_AddActivityLog.Designer.cs | 0 .../Migrations/20200514181226_AddActivityLog.cs | 0 .../Migrations/20200613202153_AddUsers.Designer.cs | 0 .../Migrations/20200613202153_AddUsers.cs | 0 ...20200728005145_AddDisplayPreferences.Designer.cs | 0 .../20200728005145_AddDisplayPreferences.cs | 0 ...905220533_FixDisplayPreferencesIndex.Designer.cs | 0 .../20200905220533_FixDisplayPreferencesIndex.cs | 0 .../20201004171403_AddMaxActiveSessions.Designer.cs | 0 .../20201004171403_AddMaxActiveSessions.cs | 0 ...04223655_AddCustomDisplayPreferences.Designer.cs | 0 .../20201204223655_AddCustomDisplayPreferences.cs | 0 ...210320181425_AddIndexesAndCollations.Designer.cs | 0 .../20210320181425_AddIndexesAndCollations.cs | 0 ...210407110544_NullableCustomPrefValue.Designer.cs | 0 .../20210407110544_NullableCustomPrefValue.cs | 0 .../20210814002109_AddDevices.Designer.cs | 0 .../Migrations/20210814002109_AddDevices.cs | 0 ...0052_AddIndexActivityLogsDateCreated.Designer.cs | 0 ...0221022080052_AddIndexActivityLogsDateCreated.cs | 0 .../20230526173516_RemoveEasyPassword.Designer.cs | 0 .../Migrations/20230526173516_RemoveEasyPassword.cs | 0 .../20230626233818_AddTrickplayInfos.Designer.cs | 0 .../Migrations/20230626233818_AddTrickplayInfos.cs | 0 .../20230923170422_UserCastReceiver.Designer.cs | 0 .../Migrations/20230923170422_UserCastReceiver.cs | 0 .../20240729140605_AddMediaSegments.Designer.cs | 0 .../Migrations/20240729140605_AddMediaSegments.cs | 0 ...930_MarkSegmentProviderIdNonNullable.Designer.cs | 0 ...240928082930_MarkSegmentProviderIdNonNullable.cs | 0 .../20241020103111_LibraryDbMigration.Designer.cs | 0 .../Migrations/20241020103111_LibraryDbMigration.cs | 0 .../20241111131257_AddedCustomDataKey.Designer.cs | 0 .../Migrations/20241111131257_AddedCustomDataKey.cs | 0 ...20241111135439_AddedCustomDataKeyKey.Designer.cs | 0 .../20241111135439_AddedCustomDataKeyKey.cs | 0 .../20241112152323_FixAncestorIdConfig.Designer.cs | 0 .../20241112152323_FixAncestorIdConfig.cs | 0 .../20241112232041_fixMediaStreams.Designer.cs | 0 .../Migrations/20241112232041_fixMediaStreams.cs | 0 .../20241112234144_FixMediaStreams2.Designer.cs | 0 .../Migrations/20241112234144_FixMediaStreams2.cs | 0 ...0241113133548_EnforceUniqueItemValue.Designer.cs | 0 .../20241113133548_EnforceUniqueItemValue.cs | 0 .../20250202021306_FixedCollation.Designer.cs | 0 .../Migrations/20250202021306_FixedCollation.cs | 0 ...50204092455_MakeStartEndDateNullable.Designer.cs | 0 .../20250204092455_MakeStartEndDateNullable.cs | 0 .../20250214031148_ChannelIdGuid.Designer.cs | 0 .../Migrations/20250214031148_ChannelIdGuid.cs | 0 .../Migrations/JellyfinDbModelSnapshot.cs | Bin .../Migrations/SqliteDesignTimeJellyfinDbFactory.cs | 0 .../ModelBuilderExtensions.cs | 0 .../Properties/AssemblyInfo.cs | 0 .../SqliteDatabaseProvider.cs | 0 .../ValueConverters/DateTimeKindValueConverter.cs | 0 .../Jellyfin.Server.Implementations.Tests.csproj | 1 - 65 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 .vscode/settings.json rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/DoNotUseReturningClauseConvention.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj => Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj} (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/.gitattributes (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20200514181226_AddActivityLog.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20200514181226_AddActivityLog.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20200613202153_AddUsers.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20200613202153_AddUsers.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20200728005145_AddDisplayPreferences.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20200728005145_AddDisplayPreferences.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20200905220533_FixDisplayPreferencesIndex.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20201004171403_AddMaxActiveSessions.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20201204223655_AddCustomDisplayPreferences.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20210320181425_AddIndexesAndCollations.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20210407110544_NullableCustomPrefValue.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20210814002109_AddDevices.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20210814002109_AddDevices.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20230526173516_RemoveEasyPassword.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20230526173516_RemoveEasyPassword.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20230626233818_AddTrickplayInfos.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20230626233818_AddTrickplayInfos.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20230923170422_UserCastReceiver.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20230923170422_UserCastReceiver.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20240729140605_AddMediaSegments.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20240729140605_AddMediaSegments.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241020103111_LibraryDbMigration.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241020103111_LibraryDbMigration.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241111131257_AddedCustomDataKey.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241111131257_AddedCustomDataKey.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241111135439_AddedCustomDataKeyKey.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241112152323_FixAncestorIdConfig.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241112232041_fixMediaStreams.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241112232041_fixMediaStreams.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241112234144_FixMediaStreams2.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241112234144_FixMediaStreams2.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20241113133548_EnforceUniqueItemValue.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20250202021306_FixedCollation.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20250202021306_FixedCollation.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20250204092455_MakeStartEndDateNullable.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20250214031148_ChannelIdGuid.Designer.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/20250214031148_ChannelIdGuid.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/JellyfinDbModelSnapshot.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Migrations/SqliteDesignTimeJellyfinDbFactory.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/ModelBuilderExtensions.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/Properties/AssemblyInfo.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/SqliteDatabaseProvider.cs (100%) rename src/Jellyfin.Database/{Jellyfin.Database.Providers.SqLite => Jellyfin.Database.Providers.Sqlite}/ValueConverters/DateTimeKindValueConverter.cs (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..6733d59aca --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.preferVisualStudioCodeFileSystemWatcher": true +} diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index 9bf67118d0..6955cf269f 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -24,7 +24,7 @@ public static class ServiceCollectionExtensions private static IDictionary GetSupportedDbProviders() { - var items = new Dictionary(); + var items = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var providerType in DatabaseProviderTypes()) { var keyAttribute = providerType.GetCustomAttribute(); @@ -34,7 +34,7 @@ public static class ServiceCollectionExtensions } var provider = providerType; - items[keyAttribute.DatabaseProviderKey.ToUpperInvariant()] = (services) => (IJellyfinDatabaseProvider)ActivatorUtilities.CreateInstance(services, providerType); + items[keyAttribute.DatabaseProviderKey] = (services) => (IJellyfinDatabaseProvider)ActivatorUtilities.CreateInstance(services, providerType); } return items; diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index e73f87d39e..b0ddfbc28c 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -36,7 +36,6 @@ - diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index bb93ef1385..d0f2bafb4e 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -198,7 +198,7 @@ namespace Jellyfin.Server _logger.LogInformation("Running query planner optimizations in the database... This might take a while"); var databaseProvider = appHost.ServiceProvider.GetRequiredService(); - var shutdownSource = new CancellationTokenSource(); + using var shutdownSource = new CancellationTokenSource(); shutdownSource.CancelAfter((int)TimeSpan.FromSeconds(60).TotalMicroseconds); await databaseProvider.RunShutdownTask(shutdownSource.Token).ConfigureAwait(false); } diff --git a/Jellyfin.sln b/Jellyfin.sln index 78f7cce09e..5d672e7ab8 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -87,7 +87,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.LiveTv.Tests", "te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.LiveTv", "src\Jellyfin.LiveTv\Jellyfin.LiveTv.csproj", "{8C6B2B13-58A4-4506-9DAB-1F882A093FE0}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jellyfin.Database", "src\Jellyfin.Database", "{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jellyfin.Database", "Jellyfin.Database", "{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Providers.SqLite", "src\Jellyfin.Database\Jellyfin.Database.Providers.SqLite\Jellyfin.Database.Providers.SqLite.csproj", "{A5590358-33CC-4B39-BDE7-DC62FEB03C76}" EndProject diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/DoNotUseReturningClauseConvention.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/DoNotUseReturningClauseConvention.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/DoNotUseReturningClauseConvention.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/DoNotUseReturningClauseConvention.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Jellyfin.Database.Providers.SqLite.csproj rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/.gitattributes b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/.gitattributes similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/.gitattributes rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/.gitattributes diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200514181226_AddActivityLog.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200514181226_AddActivityLog.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200514181226_AddActivityLog.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200514181226_AddActivityLog.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200514181226_AddActivityLog.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200613202153_AddUsers.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200613202153_AddUsers.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200613202153_AddUsers.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200613202153_AddUsers.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200613202153_AddUsers.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200728005145_AddDisplayPreferences.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200728005145_AddDisplayPreferences.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200728005145_AddDisplayPreferences.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200905220533_FixDisplayPreferencesIndex.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20200905220533_FixDisplayPreferencesIndex.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200905220533_FixDisplayPreferencesIndex.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201004171403_AddMaxActiveSessions.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201004171403_AddMaxActiveSessions.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201004171403_AddMaxActiveSessions.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201204223655_AddCustomDisplayPreferences.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20201204223655_AddCustomDisplayPreferences.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201204223655_AddCustomDisplayPreferences.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210320181425_AddIndexesAndCollations.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210320181425_AddIndexesAndCollations.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210320181425_AddIndexesAndCollations.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210407110544_NullableCustomPrefValue.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210407110544_NullableCustomPrefValue.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210407110544_NullableCustomPrefValue.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210814002109_AddDevices.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210814002109_AddDevices.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210814002109_AddDevices.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20210814002109_AddDevices.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210814002109_AddDevices.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230526173516_RemoveEasyPassword.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230526173516_RemoveEasyPassword.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230526173516_RemoveEasyPassword.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230626233818_AddTrickplayInfos.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230626233818_AddTrickplayInfos.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230626233818_AddTrickplayInfos.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230923170422_UserCastReceiver.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230923170422_UserCastReceiver.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230923170422_UserCastReceiver.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20230923170422_UserCastReceiver.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230923170422_UserCastReceiver.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240729140605_AddMediaSegments.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240729140605_AddMediaSegments.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240729140605_AddMediaSegments.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240729140605_AddMediaSegments.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240729140605_AddMediaSegments.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241020103111_LibraryDbMigration.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111131257_AddedCustomDataKey.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241111135439_AddedCustomDataKeyKey.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112152323_FixAncestorIdConfig.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112232041_fixMediaStreams.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241112234144_FixMediaStreams2.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20241113133548_EnforceUniqueItemValue.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250202021306_FixedCollation.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.Designer.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.Designer.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.Designer.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250214031148_ChannelIdGuid.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ModelBuilderExtensions.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ModelBuilderExtensions.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ModelBuilderExtensions.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ModelBuilderExtensions.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Properties/AssemblyInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Properties/AssemblyInfo.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Properties/AssemblyInfo.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Properties/AssemblyInfo.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/SqliteDatabaseProvider.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ValueConverters/DateTimeKindValueConverter.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ValueConverters/DateTimeKindValueConverter.cs similarity index 100% rename from src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/ValueConverters/DateTimeKindValueConverter.cs rename to src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ValueConverters/DateTimeKindValueConverter.cs diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index b54e2baf6a..4e2604e6e1 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -30,7 +30,6 @@ - From 8e9b57aea95a129dd730543e9b06e7c4ccdbf7f3 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 24 Mar 2025 10:14:16 +0000 Subject: [PATCH 110/508] Fixed naming scheme --- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../Jellyfin.Server.Implementations.csproj | 1 + Jellyfin.sln | 2 +- .../DoNotUseReturningClauseConvention.cs | 2 +- .../Migrations/SqliteDesignTimeJellyfinDbFactory.cs | 2 +- .../Properties/AssemblyInfo.cs | 2 +- .../SqliteDatabaseProvider.cs | 2 +- src/Jellyfin.Database/readme.md | 2 +- 8 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index 6955cf269f..b0e4567a78 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; -using Jellyfin.Database.Providers.SqLite; +using Jellyfin.Database.Providers.Sqlite; using Jellyfin.Server.Implementations.DatabaseConfiguration; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index b0ddfbc28c..6693ab8dbd 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -36,6 +36,7 @@ + diff --git a/Jellyfin.sln b/Jellyfin.sln index 5d672e7ab8..cdc8c8f651 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -89,7 +89,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.LiveTv", "src\Jell EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jellyfin.Database", "Jellyfin.Database", "{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Providers.SqLite", "src\Jellyfin.Database\Jellyfin.Database.Providers.SqLite\Jellyfin.Database.Providers.SqLite.csproj", "{A5590358-33CC-4B39-BDE7-DC62FEB03C76}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Providers.Sqlite", "src\Jellyfin.Database\Jellyfin.Database.Providers.Sqlite\Jellyfin.Database.Providers.Sqlite.csproj", "{A5590358-33CC-4B39-BDE7-DC62FEB03C76}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Implementations", "src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj", "{8C9F9221-8415-496C-B1F5-E7756F03FA59}" EndProject diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/DoNotUseReturningClauseConvention.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/DoNotUseReturningClauseConvention.cs index 1ce2420e43..18f336dda8 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/DoNotUseReturningClauseConvention.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/DoNotUseReturningClauseConvention.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Conventions; -namespace Jellyfin.Database.Providers.SqLite; +namespace Jellyfin.Database.Providers.Sqlite; internal class DoNotUseReturningClauseConvention : IModelFinalizingConvention { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs index 11eeb8e02d..448bd2fc8b 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs @@ -1,4 +1,4 @@ -using Jellyfin.Database.Providers.SqLite; +using Jellyfin.Database.Providers.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Logging.Abstractions; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Properties/AssemblyInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Properties/AssemblyInfo.cs index 8d6920f2e8..6c5c7107ea 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Properties/AssemblyInfo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Properties/AssemblyInfo.cs @@ -6,7 +6,7 @@ using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("Jellyfin.Database.Providers.SqLite")] +[assembly: AssemblyTitle("Jellyfin.Database.Providers.Sqlite")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Jellyfin Project")] diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index 2364186b12..2aa3522d8a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -8,7 +8,7 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace Jellyfin.Database.Providers.SqLite; +namespace Jellyfin.Database.Providers.Sqlite; /// /// Configures jellyfin to use an SQLite database. diff --git a/src/Jellyfin.Database/readme.md b/src/Jellyfin.Database/readme.md index d50636b779..344ba2f4d1 100644 --- a/src/Jellyfin.Database/readme.md +++ b/src/Jellyfin.Database/readme.md @@ -17,7 +17,7 @@ with sqlite currently being the only supported provider, you need to run the Ent The example is made from the root folder of the project e.g for codespaces `/workspaces/jellyfin` ```cmd -dotnet ef migrations add {MIGRATION_NAME} --project "src/Jellyfin.Database/Jellyfin.Database.Providers.SqLite" -- --migration-provider Jellyfin-SQLite +dotnet ef migrations add {MIGRATION_NAME} --project "src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite" -- --migration-provider Jellyfin-SQLite ``` If you get the error: `Run "dotnet tool restore" to make the "dotnet-ef" command available.` Run `dotnet restore`. From 8d49a396e8f68b3940d82b0018f882a37df245d9 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 24 Mar 2025 10:15:28 +0000 Subject: [PATCH 111/508] Fixed readme --- src/Jellyfin.Database/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Jellyfin.Database/readme.md b/src/Jellyfin.Database/readme.md index 344ba2f4d1..c2d2282b2f 100644 --- a/src/Jellyfin.Database/readme.md +++ b/src/Jellyfin.Database/readme.md @@ -2,7 +2,7 @@ This shall provide context on how to work with entity frameworks multi provider migration feature. -Jellyfin will support multiple database providers in the future, namely SqLite as its default and the experimental postgresSQL. +Jellyfin will support multiple database providers in the future, namely SQLite as its default and the experimental postgresSQL. Each provider has its own set of migrations, as they contain provider specific instructions to migrate the specific changes to their respective systems. @@ -12,7 +12,7 @@ When creating a new migration, you always have to create migrations for all prov dotnet ef migrations add MIGRATION_NAME --project "PATH_TO_PROJECT" -- --provider PROVIDER_KEY ``` -with sqlite currently being the only supported provider, you need to run the Entity Framework tool with the correct project to tell EFCore where to store the migrations and the correct provider key to tell jellyfin to load that provider. +with SQLite currently being the only supported provider, you need to run the Entity Framework tool with the correct project to tell EFCore where to store the migrations and the correct provider key to tell Jellyfin to load that provider. The example is made from the root folder of the project e.g for codespaces `/workspaces/jellyfin` From ef7f6fc8a97118df7f410a7afa2f501f3f4ca3e2 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 24 Mar 2025 10:16:25 +0000 Subject: [PATCH 112/508] fixed typo --- src/Jellyfin.Database/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jellyfin.Database/readme.md b/src/Jellyfin.Database/readme.md index c2d2282b2f..d320b4d5e8 100644 --- a/src/Jellyfin.Database/readme.md +++ b/src/Jellyfin.Database/readme.md @@ -2,7 +2,7 @@ This shall provide context on how to work with entity frameworks multi provider migration feature. -Jellyfin will support multiple database providers in the future, namely SQLite as its default and the experimental postgresSQL. +Jellyfin will support multiple database providers in the future, namely SQLite as its default and the experimental PostgreSQL. Each provider has its own set of migrations, as they contain provider specific instructions to migrate the specific changes to their respective systems. From daf8eca8ae7010b52538eb7bb32b82075ea0ecdc Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 24 Mar 2025 16:56:36 +0000 Subject: [PATCH 113/508] update header api description and values --- .../Filters/RetryOnTemporarlyUnavailableFilter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs b/Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs index 584519441a..74470eda0d 100644 --- a/Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs +++ b/Jellyfin.Server/Filters/RetryOnTemporarlyUnavailableFilter.cs @@ -16,11 +16,11 @@ internal class RetryOnTemporarlyUnavailableFilter : IOperationFilter Headers = new Dictionary() { { - "retry-after", - new() { AllowEmptyValue = true, Required = false, Description = "A hint for when to retry the operation in full secounds." } + "Retry-After", + new() { AllowEmptyValue = true, Required = false, Description = "A hint for when to retry the operation in full seconds." } }, { - "message", + "Message", new() { AllowEmptyValue = true, Required = false, Description = "A short plain-text reason why the server is not available." } } }, From cd5f18a08456886af496de358e6378c82625d60d Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 24 Mar 2025 17:23:16 +0000 Subject: [PATCH 114/508] Fix Cleanup task not awaiting async methods --- .../Tasks/CleanupCollectionAndPlaylistPathsTask.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs index 316e4a8f0a..8901390aae 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs @@ -84,7 +84,7 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask var collection = collections[index]; _logger.LogDebug("Checking boxset {CollectionName}", collection.Name); - CleanupLinkedChildren(collection, cancellationToken); + await CleanupLinkedChildrenAsync(collection, cancellationToken).ConfigureAwait(false); progress.Report(50D / collections.Length * (index + 1)); } } @@ -104,12 +104,12 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask var playlist = playlists[index]; _logger.LogDebug("Checking playlist {PlaylistName}", playlist.Name); - CleanupLinkedChildren(playlist, cancellationToken); + await CleanupLinkedChildrenAsync(playlist, cancellationToken).ConfigureAwait(false); progress.Report(50D / playlists.Length * (index + 1)); } } - private void CleanupLinkedChildren(T folder, CancellationToken cancellationToken) + private async Task CleanupLinkedChildrenAsync(T folder, CancellationToken cancellationToken) where T : Folder { List? itemsToRemove = null; @@ -127,8 +127,8 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask { _logger.LogDebug("Updating {FolderName}", folder.Name); folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray(); - _providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit); - folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken); + await _providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit).ConfigureAwait(false); + await folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } } From 160020c551f71441fec093f5a6b2ca2650d9a74d Mon Sep 17 00:00:00 2001 From: JPVenson Date: Tue, 25 Mar 2025 15:30:22 +0000 Subject: [PATCH 115/508] WIP fixed namespaces --- .../ApplicationHost.cs | 2 +- .../Data/CleanDatabaseScheduledTask.cs | 2 +- .../HttpServer/Security/AuthService.cs | 2 +- .../Images/BaseFolderImageProvider.cs | 1 + .../Images/CollectionFolderImageProvider.cs | 1 + .../Images/GenreImageProvider.cs | 1 + .../Images/MusicGenreImageProvider.cs | 1 + .../Library/LibraryManager.cs | 1 + .../Library/MediaSourceManager.cs | 1 + .../Library/MediaStreamSelector.cs | 2 +- .../Library/MusicManager.cs | 1 + .../Library/SearchEngine.cs | 1 + .../Library/SplashscreenPostScanTask.cs | 1 + .../Library/UserDataManager.cs | 2 +- .../Library/UserViewManager.cs | 1 + .../Validators/CollectionPostScanTask.cs | 1 + .../Tasks/OptimizeDatabaseTask.cs | 2 +- .../Session/SessionManager.cs | 1 + .../TV/TVSeriesManager.cs | 1 + .../Auth/CustomAuthenticationHandler.cs | 2 +- .../DefaultAuthorizationHandler.cs | 2 +- .../SyncPlayAccessHandler.cs | 1 + .../UserPermissionRequirement.cs | 2 +- Jellyfin.Api/Controllers/ArtistsController.cs | 1 + .../Controllers/ChannelsController.cs | 1 + .../DisplayPreferencesController.cs | 2 +- Jellyfin.Api/Controllers/GenresController.cs | 1 + Jellyfin.Api/Controllers/ItemsController.cs | 1 + Jellyfin.Api/Controllers/LibraryController.cs | 1 + Jellyfin.Api/Controllers/LiveTvController.cs | 1 + .../Controllers/MediaSegmentsController.cs | 2 +- Jellyfin.Api/Controllers/MoviesController.cs | 1 + .../Controllers/MusicGenresController.cs | 1 + .../Controllers/SuggestionsController.cs | 1 + .../Controllers/TrailersController.cs | 1 + Jellyfin.Api/Controllers/TvShowsController.cs | 1 + Jellyfin.Api/Controllers/UserController.cs | 2 +- Jellyfin.Api/Controllers/YearsController.cs | 1 + Jellyfin.Api/Helpers/MediaInfoHelper.cs | 1 + Jellyfin.Api/Helpers/RequestHelpers.cs | 1 + .../Models/LiveTvDtos/GetProgramsDto.cs | 1 + .../ActivityLogWebSocketListener.cs | 2 +- .../SessionInfoWebSocketListener.cs | 2 +- Jellyfin.Data/DayOfWeekHelper.cs | 2 +- Jellyfin.Data/UserEntityExtensions.cs | 4 +- .../Activity/ActivityManager.cs | 1 + .../DatabaseConfigurationStore.cs | 1 + .../Devices/DeviceManager.cs | 3 +- .../Extensions/ServiceCollectionExtensions.cs | 5 +- .../Item/BaseItemRepository.cs | 2 + .../Item/ChapterRepository.cs | 1 + .../Item/MediaAttachmentRepository.cs | 1 + .../Item/MediaStreamRepository.cs | 1 + .../Item/PeopleRepository.cs | 1 + .../MediaSegments/MediaSegmentManager.cs | 3 +- .../Security/AuthenticationManager.cs | 1 + .../Security/AuthorizationContext.cs | 1 + .../Trickplay/TrickplayManager.cs | 1 + .../Users/DeviceAccessHost.cs | 2 +- .../Users/DisplayPreferencesManager.cs | 1 + .../Users/UserManager.cs | 2 + Jellyfin.Server/CoreAppHost.cs | 2 +- .../ApiServiceCollectionExtensions.cs | 1 + .../Routines/MigrateActivityLogDb.cs | 2 +- .../Routines/MigrateAuthenticationDb.cs | 2 +- .../Routines/MigrateDisplayPreferencesDb.cs | 4 +- .../Migrations/Routines/MigrateLibraryDb.cs | 2 +- .../Migrations/Routines/MigrateUserDb.cs | 4 +- Jellyfin.Server/Program.cs | 2 +- Jellyfin.Server/Startup.cs | 2 +- MediaBrowser.Controller/Channels/Channel.cs | 2 +- .../Entities/Audio/MusicAlbum.cs | 1 + .../Entities/Audio/MusicArtist.cs | 1 + MediaBrowser.Controller/Entities/BaseItem.cs | 1 + MediaBrowser.Controller/Entities/Folder.cs | 1 + .../Entities/InternalItemsQuery.cs | 1 + .../Entities/Movies/BoxSet.cs | 1 + MediaBrowser.Controller/Entities/TV/Series.cs | 1 + .../Entities/UserViewBuilder.cs | 1 + .../Library/ILibraryManager.cs | 1 + .../MediaEncoding/EncodingHelper.cs | 1 + .../MediaSegments/IMediaSegmentManager.cs | 2 +- MediaBrowser.Controller/Playlists/Playlist.cs | 1 + .../Transcoding/TranscodeManager.cs | 2 +- .../Configuration/UserConfiguration.cs | 2 +- .../Dto/DisplayPreferencesDto.cs | 2 +- .../LiveTv/LiveTvChannelQuery.cs | 1 + MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs | 2 +- .../MediaSegments/MediaSegmentDto.cs | 2 +- MediaBrowser.Model/Users/UserPolicy.cs | 1 + .../DatabaseConfigurationOptions.cs | 4 +- .../Entities/AccessSchedule.cs | 2 +- .../Entities/ActivityLog.cs | 2 +- .../Entities/BaseItemEntity.cs | 2 - .../Entities/BaseItemImageInfo.cs | 1 - .../Entities/BaseItemProvider.cs | 3 - .../Entities/DisplayPreferences.cs | 2 +- .../Entities/Group.cs | 4 +- .../Entities/HomeSection.cs | 2 +- .../Entities/ItemDisplayPreferences.cs | 2 +- .../Entities/ItemValueMap.cs | 1 - .../Entities/Libraries/Artwork.cs | 4 +- .../Entities/Libraries/Book.cs | 2 +- .../Entities/Libraries/BookMetadata.cs | 2 +- .../Entities/Libraries/Chapter.cs | 2 +- .../Entities/Libraries/Collection.cs | 2 +- .../Entities/Libraries/CollectionItem.cs | 2 +- .../Entities/Libraries/Company.cs | 2 +- .../Entities/Libraries/CustomItem.cs | 2 +- .../Entities/Libraries/Episode.cs | 2 +- .../Entities/Libraries/Genre.cs | 2 +- .../Entities/Libraries/ItemMetadata.cs | 2 +- .../Entities/Libraries/Library.cs | 2 +- .../Entities/Libraries/LibraryItem.cs | 2 +- .../Entities/Libraries/MediaFile.cs | 4 +- .../Entities/Libraries/MediaFileStream.cs | 2 +- .../Entities/Libraries/MetadataProvider.cs | 2 +- .../Entities/Libraries/MetadataProviderId.cs | 2 +- .../Entities/Libraries/Movie.cs | 2 +- .../Entities/Libraries/MovieMetadata.cs | 2 +- .../Entities/Libraries/Person.cs | 2 +- .../Entities/Libraries/PersonRole.cs | 4 +- .../Entities/Libraries/Photo.cs | 2 +- .../Entities/Libraries/Rating.cs | 2 +- .../Entities/Libraries/RatingSource.cs | 2 +- .../Entities/Libraries/Release.cs | 2 +- .../Entities/Libraries/SeriesMetadata.cs | 2 +- .../Entities/Libraries/Track.cs | 2 +- .../Entities/MediaSegment.cs | 2 +- .../Entities/MediaStreamInfo.cs | 1 - .../Entities/Permission.cs | 4 +- .../Entities/Preference.cs | 4 +- .../Entities/User.cs | 6 +- .../Entities/UserData.cs | 1 - .../Enums/ArtKind.cs | 49 +++-- .../Enums/ChromecastVersion.cs | 25 ++- .../Enums/DynamicDayOfWeek.cs | 89 ++++---- .../Enums/HomeSectionType.cs | 89 ++++---- .../Enums/IndexingKind.cs | 33 ++- .../Enums/MediaFileKind.cs | 49 +++-- .../Enums/MediaSegmentType.cs | 2 +- .../Enums/PermissionKind.cs | 201 +++++++++--------- .../Enums/PersonRoleType.cs | 105 +++++---- .../Enums/PreferenceKind.cs | 113 +++++----- .../Enums/ScrollDirection.cs | 25 ++- .../Enums/SortOrder.cs | 25 ++- .../Enums/SubtitlePlaybackMode.cs | 49 +++-- .../Enums/SyncPlayUserAccessType.cs | 33 ++- .../Enums/ViewType.cs | 177 ++++++++------- .../IJellyfinDatabaseProvider.cs | 3 +- .../Interfaces/IHasArtwork.cs | 2 +- .../Interfaces/IHasCompanies.cs | 2 +- .../Interfaces/IHasConcurrencyToken.cs | 25 ++- .../Interfaces/IHasPermissions.cs | 18 +- .../Interfaces/IHasReleases.cs | 17 +- .../JellyfinDatabaseProviderKeyAttribute.cs | 4 +- .../JellyfinDbContext.cs | 6 +- .../ActivityLogConfiguration.cs | 2 +- .../AncestorIdConfiguration.cs | 3 +- .../ModelConfiguration/ApiKeyConfiguration.cs | 2 +- .../AttachmentStreamInfoConfiguration.cs | 2 +- .../BaseItemConfiguration.cs | 2 +- .../BaseItemMetadataFieldConfiguration.cs | 2 +- .../BaseItemProviderConfiguration.cs | 3 +- .../BaseItemTrailerTypeConfiguration.cs | 2 +- .../ChapterConfiguration.cs | 3 +- ...stomItemDisplayPreferencesConfiguration.cs | 2 +- .../ModelConfiguration/DeviceConfiguration.cs | 2 +- .../DeviceOptionsConfiguration.cs | 2 +- .../DisplayPreferencesConfiguration.cs | 2 +- .../ItemValuesConfiguration.cs | 3 +- .../ItemValuesMapConfiguration.cs | 3 +- .../MediaStreamInfoConfiguration.cs | 3 +- .../PeopleBaseItemMapConfiguration.cs | 3 +- .../ModelConfiguration/PeopleConfiguration.cs | 3 +- .../PermissionConfiguration.cs | 2 +- .../PreferenceConfiguration.cs | 2 +- .../TrickplayInfoConfiguration.cs | 2 +- .../ModelConfiguration/UserConfiguration.cs | 2 +- .../UserDataConfiguration.cs | 3 +- .../20200514181226_AddActivityLog.Designer.cs | 2 +- .../20200613202153_AddUsers.Designer.cs | 2 +- ...28005145_AddDisplayPreferences.Designer.cs | 2 +- ...533_FixDisplayPreferencesIndex.Designer.cs | 2 +- ...004171403_AddMaxActiveSessions.Designer.cs | 2 +- ...55_AddCustomDisplayPreferences.Designer.cs | 2 +- ...181425_AddIndexesAndCollations.Designer.cs | 2 +- ...110544_NullableCustomPrefValue.Designer.cs | 2 +- .../20210814002109_AddDevices.Designer.cs | 2 +- ...ddIndexActivityLogsDateCreated.Designer.cs | 2 +- ...30526173516_RemoveEasyPassword.Designer.cs | 2 +- ...230626233818_AddTrickplayInfos.Designer.cs | 2 +- ...0230923170422_UserCastReceiver.Designer.cs | 2 +- ...0240729140605_AddMediaSegments.Designer.cs | 2 +- ...rkSegmentProviderIdNonNullable.Designer.cs | 2 +- ...41020103111_LibraryDbMigration.Designer.cs | 2 +- ...41111131257_AddedCustomDataKey.Designer.cs | 2 +- ...11135439_AddedCustomDataKeyKey.Designer.cs | 2 +- ...1112152323_FixAncestorIdConfig.Designer.cs | 2 +- ...20241112232041_fixMediaStreams.Designer.cs | 2 +- ...0241112234144_FixMediaStreams2.Designer.cs | 2 +- ...3133548_EnforceUniqueItemValue.Designer.cs | 2 +- .../20250202021306_FixedCollation.Designer.cs | 2 +- ...92455_MakeStartEndDateNullable.Designer.cs | 2 +- .../20250214031148_ChannelIdGuid.Designer.cs | 2 +- .../Migrations/JellyfinDbModelSnapshot.cs | Bin 54722 -> 54724 bytes .../SqliteDesignTimeJellyfinDbFactory.cs | 1 + .../ModelBuilderExtensions.cs | 59 +++-- .../SqliteDatabaseProvider.cs | 2 +- .../Channels/ChannelManager.cs | 1 + src/Jellyfin.LiveTv/DefaultLiveTvService.cs | 1 + src/Jellyfin.LiveTv/LiveTvManager.cs | 1 + .../Recordings/RecordingNotifier.cs | 2 +- .../Recordings/RecordingsManager.cs | 1 + .../Auth/CustomAuthenticationHandlerTests.cs | 2 +- .../FirstTimeSetupHandlerTests.cs | 2 +- .../IgnoreScheduleHandlerTests.cs | 2 +- .../Helpers/RequestHelpersTests.cs | 1 + tests/Jellyfin.Api.Tests/TestHelpers.cs | 2 +- 219 files changed, 792 insertions(+), 772 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 6e8a9fc204..4d959905d9 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -35,11 +35,11 @@ using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; +using Jellyfin.Database.Implementations; using Jellyfin.Drawing; using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.Networking.Manager; using Jellyfin.Networking.Udp; -using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Item; using Jellyfin.Server.Implementations.MediaSegments; using MediaBrowser.Common; diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index a83ded439c..63481b1f8c 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -4,7 +4,7 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 82945a4f62..8a79cdebc1 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Jellyfin.Data; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs index f9c10ba098..0d63b3af7d 100644 --- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index 34c722e41d..273d356a39 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs index c9b41f8193..706de60a90 100644 --- a/Emby.Server.Implementations/Images/GenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs index 31f053f065..c472623e67 100644 --- a/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/MusicGenreImageProvider.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index b0003ed414..846663900b 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -21,6 +21,7 @@ using Emby.Server.Implementations.Sorting; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 3e71b2fcde..8631958365 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -16,6 +16,7 @@ using AsyncKeyedLock; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index 6791e3ca90..631179ffcf 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Model.Entities; diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 71c69ec50a..ffbf8068f0 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 3ac1d02192..9253a9a69f 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs index 76e564d535..0c9edd8398 100644 --- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs +++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index a41ef888b0..2a28131519 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -7,8 +7,8 @@ using System.Globalization; using System.Linq; using System.Threading; using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations; using Jellyfin.Extensions; -using Jellyfin.Server.Implementations; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 2560466c10..22baafbb07 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -9,6 +9,7 @@ using System.Threading; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs index 89f64ee4f0..337b1afdd4 100644 --- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs index 05223d28ae..4d3a04377f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.EntityFrameworkCore; diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index dc37c4f2a2..959373fec8 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -13,6 +13,7 @@ using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 270c1b4411..74db077d8c 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -6,6 +6,7 @@ using System.Linq; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index 0fd0149904..f6f2f59c52 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -4,7 +4,7 @@ using System.Text.Encodings.Web; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Data; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Authentication; diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index 07dedb017a..6b80d537fb 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Data; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs index 5fcf72fb46..7efb5b1698 100644 --- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs +++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessHandler.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.SyncPlay; diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs index a7c3cce971..152c400cde 100644 --- a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs +++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs @@ -1,5 +1,5 @@ using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; namespace Jellyfin.Api.Auth.UserPermissionPolicy { diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 10556da65d..2da4839138 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 2f55e88ec4..880b3a82d4 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 6d94d96f3a..2196616dd9 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -5,7 +5,7 @@ using System.Globalization; using System.Linq; using Jellyfin.Api.Helpers; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Model.Dto; diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index f0d17decbf..1fd57eba9c 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index e6fe7df79c..803c2f1f78 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 7c6160fc49..ff4540f58f 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -13,6 +13,7 @@ using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 1c0a6af79d..5461d12fa2 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -15,6 +15,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs index 2d1d4e2c8a..e30e2b54e4 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Extensions; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index cbbaaddbfe..09a7b73b94 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -7,6 +7,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index e8bc8f2657..0cb20e433d 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 9b56d08494..5075d91be7 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -5,6 +5,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 7ee4396bba..3e4bac89a5 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -1,6 +1,7 @@ using System; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index cc070244b1..0f08854d24 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -7,6 +7,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 838578fab8..d0ced277a0 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -8,7 +8,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 2b32ae728c..bbfa270db9 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 2c45789d34..1801b6bfd4 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -10,6 +10,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index eb83a37ba4..3c2691cb5e 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -7,6 +7,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs index dece664262..2616694d83 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs index e2e7b0cb5a..60379f4152 100644 --- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs @@ -1,8 +1,8 @@ using System; using System.Threading.Tasks; using Jellyfin.Data; -using Jellyfin.Data.Enums; using Jellyfin.Data.Events; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Activity; diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index cc0792477e..9d149cc85a 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Jellyfin.Data; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; diff --git a/Jellyfin.Data/DayOfWeekHelper.cs b/Jellyfin.Data/DayOfWeekHelper.cs index 82abfb8313..836860e0ea 100644 --- a/Jellyfin.Data/DayOfWeekHelper.cs +++ b/Jellyfin.Data/DayOfWeekHelper.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 using System; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; namespace Jellyfin.Data { diff --git a/Jellyfin.Data/UserEntityExtensions.cs b/Jellyfin.Data/UserEntityExtensions.cs index 8d84a6b6e1..8bf82265c9 100644 --- a/Jellyfin.Data/UserEntityExtensions.cs +++ b/Jellyfin.Data/UserEntityExtensions.cs @@ -2,8 +2,8 @@ using System; using System.ComponentModel; using System.Linq; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Enums; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data; diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 54272aeafa..007a468bf2 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; +using Jellyfin.Database.Implementations; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Querying; using Microsoft.EntityFrameworkCore; diff --git a/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationStore.cs b/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationStore.cs index 180561fc84..537630561c 100644 --- a/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationStore.cs +++ b/Jellyfin.Server.Implementations/DbConfiguration/DatabaseConfigurationStore.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Jellyfin.Database.Implementations.DbConfiguration; using MediaBrowser.Common.Configuration; namespace Jellyfin.Server.Implementations.DatabaseConfiguration; diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index 1b4048b8e6..e414a8232f 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -7,9 +7,10 @@ using Jellyfin.Data; using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; -using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Devices; diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs index b0e4567a78..fbbb5bca73 100644 --- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs +++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs @@ -1,14 +1,15 @@ using System; using System.Collections.Generic; using System.Reflection; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.DbConfiguration; using Jellyfin.Database.Providers.Sqlite; -using Jellyfin.Server.Implementations.DatabaseConfiguration; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using JellyfinDbProviderFactory = System.Func; +using JellyfinDbProviderFactory = System.Func; namespace Jellyfin.Server.Implementations.Extensions; diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index bea69b2820..1f04b28290 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -18,6 +18,8 @@ using System.Text.Json; using System.Threading; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Common; diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs index fc6f04d56a..48b94a5f3f 100644 --- a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs +++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Dto; diff --git a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs index 1557982093..18167cc530 100644 --- a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Linq; using System.Threading; using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using Microsoft.EntityFrameworkCore; diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs index f47e3fdfd3..f700718841 100644 --- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Linq; using System.Threading; using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations; using MediaBrowser.Controller; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index a8dfd4cd3a..01a0ade63c 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index 59ec418ce7..fa507ad040 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -6,7 +6,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs index 1c9f54ab03..534e80f4e7 100644 --- a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs +++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities.Security; +using Jellyfin.Database.Implementations; using MediaBrowser.Controller.Security; using Microsoft.EntityFrameworkCore; diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index 9e225393c4..e3fe517c49 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Net; using System.Threading.Tasks; using Jellyfin.Data.Queries; +using Jellyfin.Database.Implementations; using Jellyfin.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 6949ec1a8c..b55d2271a6 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs index 27222a183c..02a52e5f25 100644 --- a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs +++ b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs @@ -2,9 +2,9 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index e204a16a64..3f9491038d 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations; using MediaBrowser.Controller; using Microsoft.EntityFrameworkCore; diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 79fa70c0b2..0105f8162d 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -12,6 +12,8 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Events.Users; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 9788119a54..f3bf6b805a 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -4,10 +4,10 @@ using System.Reflection; using Emby.Server.Implementations; using Emby.Server.Implementations.Session; using Jellyfin.Api.WebSocketListeners; +using Jellyfin.Database.Implementations; using Jellyfin.Drawing; using Jellyfin.Drawing.Skia; using Jellyfin.LiveTv; -using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Devices; using Jellyfin.Server.Implementations.Events; diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 597643ed19..c3b02ad4ed 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -19,6 +19,7 @@ using Jellyfin.Api.Controllers; using Jellyfin.Api.Formatters; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions.Json; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index 2f23cb1f8f..933d85de01 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using MediaBrowser.Controller; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index c845beef2f..a50990ac5d 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities.Security; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using Microsoft.Data.Sqlite; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index 502a37cde1..4f6c5100d9 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -6,8 +6,8 @@ using System.Text.Json; using System.Text.Json.Serialization; using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index f183bce107..490daae42b 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -12,8 +12,8 @@ using System.Text; using System.Threading; using Emby.Server.Implementations.Data; using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations; using Jellyfin.Extensions; -using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Item; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index f126230fb4..bd5bf98e0f 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -3,9 +3,9 @@ using System.IO; using Emby.Server.Implementations.Data; using Jellyfin.Data; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions.Json; -using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index d8684f6da7..32814393cf 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -8,9 +8,9 @@ using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; +using Jellyfin.Database.Implementations; using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; -using Jellyfin.Server.Implementations; using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index fa21d25664..688b169359 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -6,6 +6,7 @@ using System.Net.Mime; using System.Text; using Emby.Server.Implementations.EntryPoints; using Jellyfin.Api.Middleware; +using Jellyfin.Database.Implementations; using Jellyfin.LiveTv.Extensions; using Jellyfin.LiveTv.Recordings; using Jellyfin.MediaEncoding.Hls.Extensions; @@ -13,7 +14,6 @@ using Jellyfin.Networking; using Jellyfin.Networking.HappyEyeballs; using Jellyfin.Server.Extensions; using Jellyfin.Server.HealthChecks; -using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Extensions; using Jellyfin.Server.Infrastructure; using MediaBrowser.Common.Net; diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index 49f8df508e..f6cbf5a00c 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -9,7 +9,7 @@ using System.Text.Json.Serialization; using System.Threading; using Jellyfin.Data; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Querying; diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 3b0938ea79..d6e6592429 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index e99479ee19..39dc909633 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index e20679084d..29481481c4 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index b2f23cc11d..d3e9da6226 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -16,6 +16,7 @@ using J2N.Collections.Generic.Extensions; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Collections; diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 0bd28154de..57a8a01131 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -6,6 +6,7 @@ using System.Linq; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 07def2e0f2..a252b7a25f 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -10,6 +10,7 @@ using System.Text.Json.Serialization; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Querying; diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index f3c252decc..470702f3e6 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 3670808673..18845ab9f6 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -9,6 +9,7 @@ using System.Linq; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.TV; diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index e4490bca3b..13915dc5c6 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 1c5d2f4e53..cf76f336c8 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -15,6 +15,7 @@ using System.Text.RegularExpressions; using System.Threading; using Jellyfin.Data; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs index 570d2bacea..1e75446e1c 100644 --- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.MediaSegments; diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 491acdc9c2..53e04066f0 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs index d35ed57b89..85bb862c77 100644 --- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs +++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs @@ -11,7 +11,7 @@ using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; using Jellyfin.Data; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs index b477f2593a..fe4b2de65f 100644 --- a/MediaBrowser.Model/Configuration/UserConfiguration.cs +++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 using System; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; namespace MediaBrowser.Model.Configuration { diff --git a/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs b/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs index 90163ae91f..54cbe65f68 100644 --- a/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs +++ b/MediaBrowser.Model/Dto/DisplayPreferencesDto.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; namespace MediaBrowser.Model.Dto { diff --git a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs index d872572b77..38e2731762 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvChannelQuery.cs @@ -3,6 +3,7 @@ using System; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; namespace MediaBrowser.Model.LiveTv { diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs index dae885775c..e93ad81d3a 100644 --- a/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs +++ b/MediaBrowser.Model/LiveTv/SeriesTimerQuery.cs @@ -1,6 +1,6 @@ #pragma warning disable CS1591 -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; namespace MediaBrowser.Model.LiveTv { diff --git a/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs b/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs index a0433fee18..6e5c7885cc 100644 --- a/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs +++ b/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs @@ -1,5 +1,5 @@ using System; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; namespace MediaBrowser.Model.MediaSegments; diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 951e057632..ba0eaf21cf 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Xml.Serialization; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule; namespace MediaBrowser.Model.Users diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs index af2ede7010..b481a106fd 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs @@ -1,6 +1,4 @@ -using System; - -namespace Jellyfin.Server.Implementations.DatabaseConfiguration; +namespace Jellyfin.Database.Implementations.DbConfiguration; /// /// Options to configure jellyfins managed database. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs index f534e49f3f..909e8750f8 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel.DataAnnotations.Schema; using System.Xml.Serialization; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; namespace Jellyfin.Data.Entities { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs index 51dd0ffb8e..3a76784052 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; using Microsoft.Extensions.Logging; namespace Jellyfin.Data.Entities diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs index e3e0e0861e..42be5b3b99 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs index 37723df116..ac6b72acea 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs @@ -1,7 +1,6 @@ #pragma warning disable CA2227 using System; -using System.Collections.Generic; namespace Jellyfin.Data.Entities; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs index 9a1565728d..c0c5e3147f 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs index f0be657691..82bf007a8a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; namespace Jellyfin.Data.Entities { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs index 09f2372893..4b343c164a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs index 8dd6e647e2..edffec4aba 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs @@ -1,5 +1,5 @@ using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; namespace Jellyfin.Data.Entities { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs index 93e6664ea4..7e75a200b6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; namespace Jellyfin.Data.Entities { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs index 94db6a011b..e80a9aec34 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; namespace Jellyfin.Data.Entities; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs index fc3c1036f0..b529da8fa7 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs @@ -1,8 +1,8 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Enums; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs index a838686d05..54c30d92ce 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs index 4a350d200e..7b1a68bb55 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs index f068338f92..cbcb9a5f54 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs index 7de6019692..8da9793f9d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs index 15b356a74e..4bd99d83a7 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs index 1abbee4458..5dc1039a1f 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs index e27d01d860..92307afecd 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs index ce2f0c6178..6379755264 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs index 3b822ee828..329b8973f5 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs index fa9276c669..401d58ff22 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs index 0db42a1c7b..17673cb1d9 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs index d889b871ed..975614be10 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs index 7b5a3af64c..0913f95be1 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Enums; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs index e24e73ecb7..3170653fec 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs index b38d6a4f1f..afaebb8e86 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs index a198f53ba3..fa36e58db7 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs index 499fafd0e1..beae325ec5 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs index 44b5f34d7f..df48ebf279 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs index 90dc55b70d..d9609f1cc0 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs index 7d40bdf448..627f74140b 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Enums; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs index 4b459432bc..094d57139c 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs index 58c8fa49ef..6b792ffb48 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs index 0f3a073244..91ee8caa16 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs index e68ab9105a..40466def1d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs index 42115802c5..28da91e472 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs index d354000337..6d6a920225 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities.Libraries { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs index 90120d7721..8c1c071e63 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs @@ -1,6 +1,6 @@ using System; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; namespace Jellyfin.Data.Entities; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs index 77816565af..b16b62b104 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs @@ -1,7 +1,6 @@ #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member using System; -using System.Diagnostics.CodeAnalysis; namespace Jellyfin.Data.Entities; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs index 6d2e68077c..c488b90e1c 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs @@ -3,8 +3,8 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Enums; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs index a6ab275d31..f4f9dd17ac 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs @@ -1,8 +1,8 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Enums; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs index f3398eeeac..aafa92b4a6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; using System.Text.Json.Serialization; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Enums; +using Jellyfin.Database.Implementations.Interfaces; namespace Jellyfin.Data.Entities { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs index 05ab6dd2d2..ced12b9e62 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel.DataAnnotations.Schema; namespace Jellyfin.Data.Entities; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs index f7a73848c8..218e97bcc0 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs @@ -1,33 +1,32 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// An enum representing types of art. +/// +public enum ArtKind { /// - /// An enum representing types of art. + /// Another type of art, not covered by the other members. /// - public enum ArtKind - { - /// - /// Another type of art, not covered by the other members. - /// - Other = 0, + Other = 0, - /// - /// A poster. - /// - Poster = 1, + /// + /// A poster. + /// + Poster = 1, - /// - /// A banner. - /// - Banner = 2, + /// + /// A banner. + /// + Banner = 2, - /// - /// A thumbnail. - /// - Thumbnail = 3, + /// + /// A thumbnail. + /// + Thumbnail = 3, - /// - /// A logo. - /// - Logo = 4 - } + /// + /// A logo. + /// + Logo = 4 } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs index c9c8a4a625..123f2fe438 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs @@ -1,18 +1,17 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// An enum representing the version of Chromecast to be used by clients. +/// +public enum ChromecastVersion { /// - /// An enum representing the version of Chromecast to be used by clients. + /// Stable Chromecast version. /// - public enum ChromecastVersion - { - /// - /// Stable Chromecast version. - /// - Stable = 0, + Stable = 0, - /// - /// Unstable Chromecast version. - /// - Unstable = 1 - } + /// + /// Unstable Chromecast version. + /// + Unstable = 1 } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs index d3d8dd8227..69a9b5816a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs @@ -1,58 +1,57 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// An enum that represents a day of the week, weekdays, weekends, or all days. +/// +public enum DynamicDayOfWeek { /// - /// An enum that represents a day of the week, weekdays, weekends, or all days. + /// Sunday. /// - public enum DynamicDayOfWeek - { - /// - /// Sunday. - /// - Sunday = 0, + Sunday = 0, - /// - /// Monday. - /// - Monday = 1, + /// + /// Monday. + /// + Monday = 1, - /// - /// Tuesday. - /// - Tuesday = 2, + /// + /// Tuesday. + /// + Tuesday = 2, - /// - /// Wednesday. - /// - Wednesday = 3, + /// + /// Wednesday. + /// + Wednesday = 3, - /// - /// Thursday. - /// - Thursday = 4, + /// + /// Thursday. + /// + Thursday = 4, - /// - /// Friday. - /// - Friday = 5, + /// + /// Friday. + /// + Friday = 5, - /// - /// Saturday. - /// - Saturday = 6, + /// + /// Saturday. + /// + Saturday = 6, - /// - /// All days of the week. - /// - Everyday = 7, + /// + /// All days of the week. + /// + Everyday = 7, - /// - /// A week day, or Monday-Friday. - /// - Weekday = 8, + /// + /// A week day, or Monday-Friday. + /// + Weekday = 8, - /// - /// Saturday and Sunday. - /// - Weekend = 9 - } + /// + /// Saturday and Sunday. + /// + Weekend = 9 } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs index 62da8c3fff..6ba57e74d5 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs @@ -1,58 +1,57 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// An enum representing the different options for the home screen sections. +/// +public enum HomeSectionType { /// - /// An enum representing the different options for the home screen sections. + /// None. /// - public enum HomeSectionType - { - /// - /// None. - /// - None = 0, + None = 0, - /// - /// My Media. - /// - SmallLibraryTiles = 1, + /// + /// My Media. + /// + SmallLibraryTiles = 1, - /// - /// My Media Small. - /// - LibraryButtons = 2, + /// + /// My Media Small. + /// + LibraryButtons = 2, - /// - /// Active Recordings. - /// - ActiveRecordings = 3, + /// + /// Active Recordings. + /// + ActiveRecordings = 3, - /// - /// Continue Watching. - /// - Resume = 4, + /// + /// Continue Watching. + /// + Resume = 4, - /// - /// Continue Listening. - /// - ResumeAudio = 5, + /// + /// Continue Listening. + /// + ResumeAudio = 5, - /// - /// Latest Media. - /// - LatestMedia = 6, + /// + /// Latest Media. + /// + LatestMedia = 6, - /// - /// Next Up. - /// - NextUp = 7, + /// + /// Next Up. + /// + NextUp = 7, - /// - /// Live TV. - /// - LiveTv = 8, + /// + /// Live TV. + /// + LiveTv = 8, - /// - /// Continue Reading. - /// - ResumeBook = 9 - } + /// + /// Continue Reading. + /// + ResumeBook = 9 } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs index 3967712b03..72ac1140cb 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs @@ -1,23 +1,22 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// An enum representing a type of indexing in a user's display preferences. +/// +public enum IndexingKind { /// - /// An enum representing a type of indexing in a user's display preferences. + /// Index by the premiere date. /// - public enum IndexingKind - { - /// - /// Index by the premiere date. - /// - PremiereDate = 0, + PremiereDate = 0, - /// - /// Index by the production year. - /// - ProductionYear = 1, + /// + /// Index by the production year. + /// + ProductionYear = 1, - /// - /// Index by the community rating. - /// - CommunityRating = 2 - } + /// + /// Index by the community rating. + /// + CommunityRating = 2 } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs index 797c26ec27..8e6f677dca 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs @@ -1,33 +1,32 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// An enum representing the type of media file. +/// +public enum MediaFileKind { /// - /// An enum representing the type of media file. + /// The main file. /// - public enum MediaFileKind - { - /// - /// The main file. - /// - Main = 0, + Main = 0, - /// - /// A sidecar file. - /// - Sidecar = 1, + /// + /// A sidecar file. + /// + Sidecar = 1, - /// - /// An additional part to the main file. - /// - AdditionalPart = 2, + /// + /// An additional part to the main file. + /// + AdditionalPart = 2, - /// - /// An alternative format to the main file. - /// - AlternativeFormat = 3, + /// + /// An alternative format to the main file. + /// + AlternativeFormat = 3, - /// - /// An additional stream for the main file. - /// - AdditionalStream = 4 - } + /// + /// An additional stream for the main file. + /// + AdditionalStream = 4 } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs index 4586354504..fed092b978 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs @@ -1,6 +1,6 @@ using Jellyfin.Data.Entities; -namespace Jellyfin.Data.Enums; +namespace Jellyfin.Database.Implementations.Enums; /// /// Defines the types of content an individual represents. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs index c3d6705c24..0818639636 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs @@ -1,128 +1,127 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// The types of user permissions. +/// +public enum PermissionKind { /// - /// The types of user permissions. + /// Whether the user is an administrator. /// - public enum PermissionKind - { - /// - /// Whether the user is an administrator. - /// - IsAdministrator = 0, + IsAdministrator = 0, - /// - /// Whether the user is hidden. - /// - IsHidden = 1, + /// + /// Whether the user is hidden. + /// + IsHidden = 1, - /// - /// Whether the user is disabled. - /// - IsDisabled = 2, + /// + /// Whether the user is disabled. + /// + IsDisabled = 2, - /// - /// Whether the user can control shared devices. - /// - EnableSharedDeviceControl = 3, + /// + /// Whether the user can control shared devices. + /// + EnableSharedDeviceControl = 3, - /// - /// Whether the user can access the server remotely. - /// - EnableRemoteAccess = 4, + /// + /// Whether the user can access the server remotely. + /// + EnableRemoteAccess = 4, - /// - /// Whether the user can manage live tv. - /// - EnableLiveTvManagement = 5, + /// + /// Whether the user can manage live tv. + /// + EnableLiveTvManagement = 5, - /// - /// Whether the user can access live tv. - /// - EnableLiveTvAccess = 6, + /// + /// Whether the user can access live tv. + /// + EnableLiveTvAccess = 6, - /// - /// Whether the user can play media. - /// - EnableMediaPlayback = 7, + /// + /// Whether the user can play media. + /// + EnableMediaPlayback = 7, - /// - /// Whether the server should transcode audio for the user if requested. - /// - EnableAudioPlaybackTranscoding = 8, + /// + /// Whether the server should transcode audio for the user if requested. + /// + EnableAudioPlaybackTranscoding = 8, - /// - /// Whether the server should transcode video for the user if requested. - /// - EnableVideoPlaybackTranscoding = 9, + /// + /// Whether the server should transcode video for the user if requested. + /// + EnableVideoPlaybackTranscoding = 9, - /// - /// Whether the user can delete content. - /// - EnableContentDeletion = 10, + /// + /// Whether the user can delete content. + /// + EnableContentDeletion = 10, - /// - /// Whether the user can download content. - /// - EnableContentDownloading = 11, + /// + /// Whether the user can download content. + /// + EnableContentDownloading = 11, - /// - /// Whether to enable sync transcoding for the user. - /// - EnableSyncTranscoding = 12, + /// + /// Whether to enable sync transcoding for the user. + /// + EnableSyncTranscoding = 12, - /// - /// Whether the user can do media conversion. - /// - EnableMediaConversion = 13, + /// + /// Whether the user can do media conversion. + /// + EnableMediaConversion = 13, - /// - /// Whether the user has access to all devices. - /// - EnableAllDevices = 14, + /// + /// Whether the user has access to all devices. + /// + EnableAllDevices = 14, - /// - /// Whether the user has access to all channels. - /// - EnableAllChannels = 15, + /// + /// Whether the user has access to all channels. + /// + EnableAllChannels = 15, - /// - /// Whether the user has access to all folders. - /// - EnableAllFolders = 16, + /// + /// Whether the user has access to all folders. + /// + EnableAllFolders = 16, - /// - /// Whether to enable public sharing for the user. - /// - EnablePublicSharing = 17, + /// + /// Whether to enable public sharing for the user. + /// + EnablePublicSharing = 17, - /// - /// Whether the user can remotely control other users. - /// - EnableRemoteControlOfOtherUsers = 18, + /// + /// Whether the user can remotely control other users. + /// + EnableRemoteControlOfOtherUsers = 18, - /// - /// Whether the user is permitted to do playback remuxing. - /// - EnablePlaybackRemuxing = 19, + /// + /// Whether the user is permitted to do playback remuxing. + /// + EnablePlaybackRemuxing = 19, - /// - /// Whether the server should force transcoding on remote connections for the user. - /// - ForceRemoteSourceTranscoding = 20, + /// + /// Whether the server should force transcoding on remote connections for the user. + /// + ForceRemoteSourceTranscoding = 20, - /// - /// Whether the user can create, modify and delete collections. - /// - EnableCollectionManagement = 21, + /// + /// Whether the user can create, modify and delete collections. + /// + EnableCollectionManagement = 21, - /// - /// Whether the user can edit subtitles. - /// - EnableSubtitleManagement = 22, + /// + /// Whether the user can edit subtitles. + /// + EnableSubtitleManagement = 22, - /// - /// Whether the user can edit lyrics. - /// - EnableLyricManagement = 23, - } + /// + /// Whether the user can edit lyrics. + /// + EnableLyricManagement = 23, } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs index 1e619f5eef..5b913385e8 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs @@ -1,68 +1,67 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// An enum representing a person's role in a specific media item. +/// +public enum PersonRoleType { /// - /// An enum representing a person's role in a specific media item. + /// Another role, not covered by the other types. /// - public enum PersonRoleType - { - /// - /// Another role, not covered by the other types. - /// - Other = 0, + Other = 0, - /// - /// The director of the media. - /// - Director = 1, + /// + /// The director of the media. + /// + Director = 1, - /// - /// An artist. - /// - Artist = 2, + /// + /// An artist. + /// + Artist = 2, - /// - /// The original artist. - /// - OriginalArtist = 3, + /// + /// The original artist. + /// + OriginalArtist = 3, - /// - /// An actor. - /// - Actor = 4, + /// + /// An actor. + /// + Actor = 4, - /// - /// A voice actor. - /// - VoiceActor = 5, + /// + /// A voice actor. + /// + VoiceActor = 5, - /// - /// A producer. - /// - Producer = 6, + /// + /// A producer. + /// + Producer = 6, - /// - /// A remixer. - /// - Remixer = 7, + /// + /// A remixer. + /// + Remixer = 7, - /// - /// A conductor. - /// - Conductor = 8, + /// + /// A conductor. + /// + Conductor = 8, - /// - /// A composer. - /// - Composer = 9, + /// + /// A composer. + /// + Composer = 9, - /// - /// An author. - /// - Author = 10, + /// + /// An author. + /// + Author = 10, - /// - /// An editor. - /// - Editor = 11 - } + /// + /// An editor. + /// + Editor = 11 } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs index d2b412e459..f70e3e2c23 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs @@ -1,73 +1,72 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// The types of user preferences. +/// +public enum PreferenceKind { /// - /// The types of user preferences. + /// A list of blocked tags. /// - public enum PreferenceKind - { - /// - /// A list of blocked tags. - /// - BlockedTags = 0, + BlockedTags = 0, - /// - /// A list of blocked channels. - /// - BlockedChannels = 1, + /// + /// A list of blocked channels. + /// + BlockedChannels = 1, - /// - /// A list of blocked media folders. - /// - BlockedMediaFolders = 2, + /// + /// A list of blocked media folders. + /// + BlockedMediaFolders = 2, - /// - /// A list of enabled devices. - /// - EnabledDevices = 3, + /// + /// A list of enabled devices. + /// + EnabledDevices = 3, - /// - /// A list of enabled channels. - /// - EnabledChannels = 4, + /// + /// A list of enabled channels. + /// + EnabledChannels = 4, - /// - /// A list of enabled folders. - /// - EnabledFolders = 5, + /// + /// A list of enabled folders. + /// + EnabledFolders = 5, - /// - /// A list of folders to allow content deletion from. - /// - EnableContentDeletionFromFolders = 6, + /// + /// A list of folders to allow content deletion from. + /// + EnableContentDeletionFromFolders = 6, - /// - /// A list of latest items to exclude. - /// - LatestItemExcludes = 7, + /// + /// A list of latest items to exclude. + /// + LatestItemExcludes = 7, - /// - /// A list of media to exclude. - /// - MyMediaExcludes = 8, + /// + /// A list of media to exclude. + /// + MyMediaExcludes = 8, - /// - /// A list of grouped folders. - /// - GroupedFolders = 9, + /// + /// A list of grouped folders. + /// + GroupedFolders = 9, - /// - /// A list of unrated items to block. - /// - BlockUnratedItems = 10, + /// + /// A list of unrated items to block. + /// + BlockUnratedItems = 10, - /// - /// A list of ordered views. - /// - OrderedViews = 11, + /// + /// A list of ordered views. + /// + OrderedViews = 11, - /// - /// A list of allowed tags. - /// - AllowedTags = 12 - } + /// + /// A list of allowed tags. + /// + AllowedTags = 12 } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs index 29c50e2c4e..3ff3c45faf 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs @@ -1,18 +1,17 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// An enum representing the axis that should be scrolled. +/// +public enum ScrollDirection { /// - /// An enum representing the axis that should be scrolled. + /// Horizontal scrolling direction. /// - public enum ScrollDirection - { - /// - /// Horizontal scrolling direction. - /// - Horizontal = 0, + Horizontal = 0, - /// - /// Vertical scrolling direction. - /// - Vertical = 1 - } + /// + /// Vertical scrolling direction. + /// + Vertical = 1 } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs index 4151448e4e..c865b75f11 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs @@ -1,18 +1,17 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// An enum representing the sorting order. +/// +public enum SortOrder { /// - /// An enum representing the sorting order. + /// Sort in increasing order. /// - public enum SortOrder - { - /// - /// Sort in increasing order. - /// - Ascending = 0, + Ascending = 0, - /// - /// Sort in decreasing order. - /// - Descending = 1 - } + /// + /// Sort in decreasing order. + /// + Descending = 1 } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs index 79693d321a..c394c209bb 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs @@ -1,33 +1,32 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// An enum representing a subtitle playback mode. +/// +public enum SubtitlePlaybackMode { /// - /// An enum representing a subtitle playback mode. + /// The default subtitle playback mode. /// - public enum SubtitlePlaybackMode - { - /// - /// The default subtitle playback mode. - /// - Default = 0, + Default = 0, - /// - /// Always show subtitles. - /// - Always = 1, + /// + /// Always show subtitles. + /// + Always = 1, - /// - /// Only show forced subtitles. - /// - OnlyForced = 2, + /// + /// Only show forced subtitles. + /// + OnlyForced = 2, - /// - /// Don't show subtitles. - /// - None = 3, + /// + /// Don't show subtitles. + /// + None = 3, - /// - /// Only show subtitles when the current audio stream is in a different language. - /// - Smart = 4 - } + /// + /// Only show subtitles when the current audio stream is in a different language. + /// + Smart = 4 } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs index 030d16fb90..311642e0d7 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs @@ -1,23 +1,22 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// Enum SyncPlayUserAccessType. +/// +public enum SyncPlayUserAccessType { /// - /// Enum SyncPlayUserAccessType. + /// User can create groups and join them. /// - public enum SyncPlayUserAccessType - { - /// - /// User can create groups and join them. - /// - CreateAndJoinGroups = 0, + CreateAndJoinGroups = 0, - /// - /// User can only join already existing groups. - /// - JoinGroups = 1, + /// + /// User can only join already existing groups. + /// + JoinGroups = 1, - /// - /// SyncPlay is disabled for the user. - /// - None = 2 - } + /// + /// SyncPlay is disabled for the user. + /// + None = 2 } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs index c0fd7d448b..b2bcbf2bb6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs @@ -1,113 +1,112 @@ -namespace Jellyfin.Data.Enums +namespace Jellyfin.Database.Implementations.Enums; + +/// +/// An enum representing the type of view for a library or collection. +/// +public enum ViewType { /// - /// An enum representing the type of view for a library or collection. + /// Shows albums. /// - public enum ViewType - { - /// - /// Shows albums. - /// - Albums = 0, + Albums = 0, - /// - /// Shows album artists. - /// - AlbumArtists = 1, + /// + /// Shows album artists. + /// + AlbumArtists = 1, - /// - /// Shows artists. - /// - Artists = 2, + /// + /// Shows artists. + /// + Artists = 2, - /// - /// Shows channels. - /// - Channels = 3, + /// + /// Shows channels. + /// + Channels = 3, - /// - /// Shows collections. - /// - Collections = 4, + /// + /// Shows collections. + /// + Collections = 4, - /// - /// Shows episodes. - /// - Episodes = 5, + /// + /// Shows episodes. + /// + Episodes = 5, - /// - /// Shows favorites. - /// - Favorites = 6, + /// + /// Shows favorites. + /// + Favorites = 6, - /// - /// Shows genres. - /// - Genres = 7, + /// + /// Shows genres. + /// + Genres = 7, - /// - /// Shows guide. - /// - Guide = 8, + /// + /// Shows guide. + /// + Guide = 8, - /// - /// Shows movies. - /// - Movies = 9, + /// + /// Shows movies. + /// + Movies = 9, - /// - /// Shows networks. - /// - Networks = 10, + /// + /// Shows networks. + /// + Networks = 10, - /// - /// Shows playlists. - /// - Playlists = 11, + /// + /// Shows playlists. + /// + Playlists = 11, - /// - /// Shows programs. - /// - Programs = 12, + /// + /// Shows programs. + /// + Programs = 12, - /// - /// Shows recordings. - /// - Recordings = 13, + /// + /// Shows recordings. + /// + Recordings = 13, - /// - /// Shows schedule. - /// - Schedule = 14, + /// + /// Shows schedule. + /// + Schedule = 14, - /// - /// Shows series. - /// - Series = 15, + /// + /// Shows series. + /// + Series = 15, - /// - /// Shows shows. - /// - Shows = 16, + /// + /// Shows shows. + /// + Shows = 16, - /// - /// Shows songs. - /// - Songs = 17, + /// + /// Shows songs. + /// + Songs = 17, - /// - /// Shows songs. - /// - Suggestions = 18, + /// + /// Shows songs. + /// + Suggestions = 18, - /// - /// Shows trailers. - /// - Trailers = 19, + /// + /// Shows trailers. + /// + Trailers = 19, - /// - /// Shows upcoming. - /// - Upcoming = 20 - } + /// + /// Shows upcoming. + /// + Upcoming = 20 } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs index cc96792e64..0740165530 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs @@ -1,9 +1,8 @@ -using System; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -namespace Jellyfin.Server.Implementations; +namespace Jellyfin.Database.Implementations; /// /// Defines the type and extension points for multi database support. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs index a4d9c54af4..03c2ca4a47 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Jellyfin.Data.Entities.Libraries; -namespace Jellyfin.Data.Interfaces +namespace Jellyfin.Database.Implementations.Interfaces { /// /// An interface abstracting an entity that has artwork. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs index 8f19ce04fa..ed449a8f14 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Jellyfin.Data.Entities.Libraries; -namespace Jellyfin.Data.Interfaces +namespace Jellyfin.Database.Implementations.Interfaces { /// /// An abstraction representing an entity that has companies. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs index 2c4091493e..196d2680db 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs @@ -1,18 +1,17 @@ -namespace Jellyfin.Data.Interfaces +namespace Jellyfin.Database.Implementations.Interfaces; + +/// +/// An interface abstracting an entity that has a concurrency token. +/// +public interface IHasConcurrencyToken { /// - /// An interface abstracting an entity that has a concurrency token. + /// Gets the version of this row. Acts as a concurrency token. /// - public interface IHasConcurrencyToken - { - /// - /// Gets the version of this row. Acts as a concurrency token. - /// - uint RowVersion { get; } + uint RowVersion { get; } - /// - /// Called when saving changes to this entity. - /// - void OnSavingChanges(); - } + /// + /// Called when saving changes to this entity. + /// + void OnSavingChanges(); } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs index 6d1eb59f67..606b1169b8 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs @@ -1,17 +1,15 @@ using System.Collections.Generic; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -namespace Jellyfin.Data.Interfaces +namespace Jellyfin.Database.Implementations.Interfaces; + +/// +/// An abstraction representing an entity that has permissions. +/// +public interface IHasPermissions { /// - /// An abstraction representing an entity that has permissions. + /// Gets a collection containing this entity's permissions. /// - public interface IHasPermissions - { - /// - /// Gets a collection containing this entity's permissions. - /// - ICollection Permissions { get; } - } + ICollection Permissions { get; } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs index 3b615893ed..653572b6e9 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs @@ -1,16 +1,15 @@ using System.Collections.Generic; using Jellyfin.Data.Entities.Libraries; -namespace Jellyfin.Data.Interfaces +namespace Jellyfin.Database.Implementations.Interfaces; + +/// +/// An abstraction representing an entity that has releases. +/// +public interface IHasReleases { /// - /// An abstraction representing an entity that has releases. + /// Gets a collection containing this entity's releases. /// - public interface IHasReleases - { - /// - /// Gets a collection containing this entity's releases. - /// - ICollection Releases { get; } - } + ICollection Releases { get; } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs index b3ab3d0944..778aca373e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Server.Implementations; +namespace Jellyfin.Database.Implementations; /// /// Defines the key of the database provider. @@ -16,7 +16,7 @@ public sealed class JellyfinDatabaseProviderKeyAttribute : System.Attribute /// The key on which to identify the annotated provider. public JellyfinDatabaseProviderKeyAttribute(string databaseProviderKey) { - this._databaseProviderKey = databaseProviderKey; + _databaseProviderKey = databaseProviderKey; } /// diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs index 024d595d58..5cdc853319 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs @@ -2,13 +2,11 @@ using System; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; -using Jellyfin.Data.Interfaces; +using Jellyfin.Database.Implementations.Interfaces; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Metadata.Conventions; using Microsoft.Extensions.Logging; -namespace Jellyfin.Server.Implementations; +namespace Jellyfin.Database.Implementations; /// /// diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs index 9a63ed9f21..be99b46d56 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// FluentAPI configuration for the ActivityLog entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs index 8cc817fb8b..146df5546c 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs @@ -1,9 +1,8 @@ -using System; using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// AncestorId configuration. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs index 3f19b6986a..7c15f064d8 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities.Security; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration +namespace Jellyfin.Database.Implementations.ModelConfiguration { /// /// FluentAPI configuration for the ApiKey entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs index 057b6689ac..b5fae4053c 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// FluentAPI configuration for the AttachmentStreamInfo entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs index 08f2a33566..67a071039f 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// Configuration for BaseItem. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs index b4c6511bf2..c101e0085e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// Provides configuration for the BaseItemMetadataField entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs index d15049a1fa..175a82e091 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs @@ -1,9 +1,8 @@ -using System; using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// BaseItemProvider configuration. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs index e9564b854b..c7efef2390 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// Provides configuration for the BaseItemMetadataField entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs index 5a84f7750a..5935b632a7 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs @@ -1,9 +1,8 @@ -using System; using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// Chapter configuration. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs index 779aec986b..550fa5073b 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration +namespace Jellyfin.Database.Implementations.ModelConfiguration { /// /// FluentAPI configuration for the CustomItemDisplayPreferences entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs index a750b65c03..5f87c8d403 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities.Security; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration +namespace Jellyfin.Database.Implementations.ModelConfiguration { /// /// FluentAPI configuration for the Device entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs index 038afd7524..923a53acaf 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities.Security; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration +namespace Jellyfin.Database.Implementations.ModelConfiguration { /// /// FluentAPI configuration for the DeviceOptions entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs index 9b437861bb..92068c8f29 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration +namespace Jellyfin.Database.Implementations.ModelConfiguration { /// /// FluentAPI configuration for the DisplayPreferencesConfiguration entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs index abeeb09c9b..0e63c130e3 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs @@ -1,9 +1,8 @@ -using System; using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// itemvalues Configuration. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs index 9c22b114c7..3860b0e161 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs @@ -1,9 +1,8 @@ -using System; using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// itemvalues Configuration. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs index 7e572f9a39..37e15f5e72 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs @@ -1,9 +1,8 @@ -using System; using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// People configuration. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs index cdaee9161c..125b9e92cd 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs @@ -1,9 +1,8 @@ -using System; using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// People configuration. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs index f3cccb13fe..fe25d3064d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs @@ -1,9 +1,8 @@ -using System; using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// People configuration. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs index 240e284c0c..90a3ec5d00 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration +namespace Jellyfin.Database.Implementations.ModelConfiguration { /// /// FluentAPI configuration for the Permission entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs index 49c869c6a8..d6d3fadfc4 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration +namespace Jellyfin.Database.Implementations.ModelConfiguration { /// /// FluentAPI configuration for the Permission entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs index dc1c17e5ef..a198c80f7e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration +namespace Jellyfin.Database.Implementations.ModelConfiguration { /// /// FluentAPI configuration for the TrickplayInfo entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs index bcaa3634ed..4e830801ee 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration +namespace Jellyfin.Database.Implementations.ModelConfiguration { /// /// FluentAPI configuration for the User entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs index 7bbb28d431..a7e223c435 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs @@ -1,9 +1,8 @@ -using System; using Jellyfin.Data.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Jellyfin.Server.Implementations.ModelConfiguration; +namespace Jellyfin.Database.Implementations.ModelConfiguration; /// /// FluentAPI configuration for the UserData entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200514181226_AddActivityLog.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200514181226_AddActivityLog.Designer.cs index 80fe784dd7..7891006430 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200514181226_AddActivityLog.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200514181226_AddActivityLog.Designer.cs @@ -2,7 +2,7 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200613202153_AddUsers.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200613202153_AddUsers.Designer.cs index 7aa4479b3d..eab3cd3e7e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200613202153_AddUsers.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200613202153_AddUsers.Designer.cs @@ -2,7 +2,7 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs index 3860c851d0..91dd0ff7ab 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200728005145_AddDisplayPreferences.Designer.cs @@ -2,7 +2,7 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs index 1134f7aa42..8ec9231030 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20200905220533_FixDisplayPreferencesIndex.Designer.cs @@ -2,7 +2,7 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs index 607310caae..499faa9c47 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs @@ -2,7 +2,7 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs index 02c3fc7532..7ab851689a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20201204223655_AddCustomDisplayPreferences.Designer.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs index 1cfd7112ce..e14ed9380e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210320181425_AddIndexesAndCollations.Designer.cs @@ -2,7 +2,7 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs index ecf7af4952..05f2c80a25 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210407110544_NullableCustomPrefValue.Designer.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210814002109_AddDevices.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210814002109_AddDevices.Designer.cs index dccba6f773..c9f3cf696c 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210814002109_AddDevices.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20210814002109_AddDevices.Designer.cs @@ -2,7 +2,7 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs index e821c106e3..ab7781d155 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs @@ -2,7 +2,7 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs index 360fa03768..8a2806113f 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230526173516_RemoveEasyPassword.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs index 17d33845f5..a11507bd5d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230626233818_AddTrickplayInfos.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230923170422_UserCastReceiver.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230923170422_UserCastReceiver.Designer.cs index 4c09176691..ddea37f6dd 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230923170422_UserCastReceiver.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20230923170422_UserCastReceiver.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240729140605_AddMediaSegments.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240729140605_AddMediaSegments.Designer.cs index 35a3cdad2d..ab7065ee65 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240729140605_AddMediaSegments.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240729140605_AddMediaSegments.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs index 8dba31a67f..aa60bff324 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.Designer.cs index 27745f601a..2ea6dafe13 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs index 1fbf21492d..d589a4afd7 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs index bac6fd5b5a..3d70bb0296 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs index ad622d44c5..1e0d3b129f 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.Designer.cs index dc4c8212ba..ccf67d899f 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.Designer.cs index 5714120b5c..d3ba8c96a9 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs index 855f02fd3f..2c0058c720 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.Designer.cs index d7b806d7a3..da4bab3fd3 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs index a329f1ef16..9b72d9688a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.Designer.cs index 48919c9b5d..f5cfe86c44 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs index 79587a9c334d4f9cc9cc4e311c8cbb9fd7d6f517..5d8ddde0820763ca335545cb5f01cd5329d3eb95 100644 GIT binary patch delta 21 dcmX@Kn)%3T<_TsTE{P?HNr}a&8*SRI0svvU2@U`N delta 19 bcmX@In)%Rb<_TtO!Kp=MsYM&@+O7fsQq2f( diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs index 448bd2fc8b..1629c732c6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs @@ -1,3 +1,4 @@ +using Jellyfin.Database.Implementations; using Jellyfin.Database.Providers.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ModelBuilderExtensions.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ModelBuilderExtensions.cs index 79ae1661aa..0d75686198 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ModelBuilderExtensions.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ModelBuilderExtensions.cs @@ -3,46 +3,45 @@ using Jellyfin.Server.Implementations.ValueConverters; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace Jellyfin.Server.Implementations +namespace Jellyfin.Database.Providers.Sqlite; + +/// +/// Model builder extensions. +/// +public static class ModelBuilderExtensions { /// - /// Model builder extensions. + /// Specify value converter for the object type. /// - public static class ModelBuilderExtensions + /// The model builder. + /// The . + /// The type to convert. + /// The modified . + public static ModelBuilder UseValueConverterForType(this ModelBuilder modelBuilder, ValueConverter converter) { - /// - /// Specify value converter for the object type. - /// - /// The model builder. - /// The . - /// The type to convert. - /// The modified . - public static ModelBuilder UseValueConverterForType(this ModelBuilder modelBuilder, ValueConverter converter) + var type = typeof(T); + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { - var type = typeof(T); - foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + foreach (var property in entityType.GetProperties()) { - foreach (var property in entityType.GetProperties()) + if (property.ClrType == type) { - if (property.ClrType == type) - { - property.SetValueConverter(converter); - } + property.SetValueConverter(converter); } } - - return modelBuilder; } - /// - /// Specify the default . - /// - /// The model builder to extend. - /// The to specify. - public static void SetDefaultDateTimeKind(this ModelBuilder modelBuilder, DateTimeKind kind) - { - modelBuilder.UseValueConverterForType(new DateTimeKindValueConverter(kind)); - modelBuilder.UseValueConverterForType(new DateTimeKindValueConverter(kind)); - } + return modelBuilder; + } + + /// + /// Specify the default . + /// + /// The model builder to extend. + /// The to specify. + public static void SetDefaultDateTimeKind(this ModelBuilder modelBuilder, DateTimeKind kind) + { + modelBuilder.UseValueConverterForType(new DateTimeKindValueConverter(kind)); + modelBuilder.UseValueConverterForType(new DateTimeKindValueConverter(kind)); } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index 2aa3522d8a..d9eb0ae7a4 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -2,7 +2,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Server.Implementations; +using Jellyfin.Database.Implementations; using MediaBrowser.Common.Configuration; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs index 83f68ab509..402a3f3b0e 100644 --- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs +++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using AsyncKeyedLock; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Extensions; diff --git a/src/Jellyfin.LiveTv/DefaultLiveTvService.cs b/src/Jellyfin.LiveTv/DefaultLiveTvService.cs index 318cc7acd0..d8f873abe6 100644 --- a/src/Jellyfin.LiveTv/DefaultLiveTvService.cs +++ b/src/Jellyfin.LiveTv/DefaultLiveTvService.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using Jellyfin.LiveTv.Configuration; using Jellyfin.LiveTv.Timers; diff --git a/src/Jellyfin.LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs index da98606a4b..7ebcc48834 100644 --- a/src/Jellyfin.LiveTv/LiveTvManager.cs +++ b/src/Jellyfin.LiveTv/LiveTvManager.cs @@ -12,6 +12,7 @@ using Jellyfin.Data; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.LiveTv.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingNotifier.cs b/src/Jellyfin.LiveTv/Recordings/RecordingNotifier.cs index 1d571805b8..a5d186ce18 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingNotifier.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingNotifier.cs @@ -3,8 +3,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data; -using Jellyfin.Data.Enums; using Jellyfin.Data.Events; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Session; diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs index 2f4caa3867..9ca5d7420b 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.LiveTv.Configuration; using Jellyfin.LiveTv.IO; using Jellyfin.LiveTv.Timers; diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index 6b3afc9357..0a6489d0ac 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -8,7 +8,7 @@ using Jellyfin.Api.Auth; using Jellyfin.Api.Constants; using Jellyfin.Data; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Authentication; diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs index 31d2b486b3..1fe9fc97ef 100644 --- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs @@ -8,7 +8,7 @@ using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.FirstTimeSetupPolicy; using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs index 534d1863ca..ed5235252a 100644 --- a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs @@ -6,7 +6,7 @@ using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs index a2d1b3607b..2851b08e6c 100644 --- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs +++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs @@ -5,6 +5,7 @@ using System.Security.Claims; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Net; using Xunit; diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs index d84da89e28..03884d7754 100644 --- a/tests/Jellyfin.Api.Tests/TestHelpers.cs +++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs @@ -6,7 +6,7 @@ using System.Security.Claims; using Jellyfin.Api.Constants; using Jellyfin.Data; using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Enums; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; From 42bdb22bfb690a6af37d70f12844881d884927b1 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Tue, 25 Mar 2025 16:45:00 +0100 Subject: [PATCH 116/508] Fixed namespaces --- .../Collections/CollectionManager.cs | 2 +- Emby.Server.Implementations/Dto/DtoService.cs | 2 +- .../EntryPoints/LibraryChangedNotifier.cs | 2 +- Emby.Server.Implementations/Library/LibraryManager.cs | 2 +- Emby.Server.Implementations/Library/MediaSourceManager.cs | 2 +- Emby.Server.Implementations/Library/MusicManager.cs | 2 +- Emby.Server.Implementations/Library/SearchEngine.cs | 2 +- Emby.Server.Implementations/Library/UserDataManager.cs | 2 +- Emby.Server.Implementations/Library/UserViewManager.cs | 2 +- Emby.Server.Implementations/Playlists/PlaylistManager.cs | 2 +- Emby.Server.Implementations/Playlists/PlaylistsFolder.cs | 2 +- Emby.Server.Implementations/Session/SessionManager.cs | 4 ++-- .../Sorting/DateLastMediaAddedComparer.cs | 2 +- Emby.Server.Implementations/Sorting/DatePlayedComparer.cs | 2 +- .../Sorting/IsFavoriteOrLikeComparer.cs | 2 +- Emby.Server.Implementations/Sorting/IsPlayedComparer.cs | 2 +- Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs | 2 +- Emby.Server.Implementations/Sorting/PlayCountComparer.cs | 2 +- Emby.Server.Implementations/SyncPlay/Group.cs | 2 +- Emby.Server.Implementations/TV/TVSeriesManager.cs | 2 +- Jellyfin.Api/Controllers/ArtistsController.cs | 2 +- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 2 +- Jellyfin.Api/Controllers/GenresController.cs | 2 +- Jellyfin.Api/Controllers/ImageController.cs | 2 +- Jellyfin.Api/Controllers/InstantMixController.cs | 2 +- Jellyfin.Api/Controllers/LibraryController.cs | 2 +- Jellyfin.Api/Controllers/MoviesController.cs | 2 +- Jellyfin.Api/Controllers/MusicGenresController.cs | 2 +- Jellyfin.Api/Controllers/PersonsController.cs | 2 +- Jellyfin.Api/Controllers/PlaystateController.cs | 2 +- Jellyfin.Api/Controllers/StudiosController.cs | 2 +- Jellyfin.Api/Controllers/SuggestionsController.cs | 2 +- Jellyfin.Api/Controllers/UserLibraryController.cs | 2 +- Jellyfin.Api/Controllers/YearsController.cs | 2 +- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 2 +- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 2 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 2 +- Jellyfin.Data/Events/Users/UserCreatedEventArgs.cs | 2 +- Jellyfin.Data/Events/Users/UserDeletedEventArgs.cs | 2 +- Jellyfin.Data/Events/Users/UserLockedOutEventArgs.cs | 2 +- Jellyfin.Data/Events/Users/UserPasswordChangedEventArgs.cs | 2 +- Jellyfin.Data/Events/Users/UserUpdatedEventArgs.cs | 2 +- Jellyfin.Data/UserEntityExtensions.cs | 2 +- .../Activity/ActivityManager.cs | 2 +- Jellyfin.Server.Implementations/Devices/DeviceManager.cs | 4 ++-- .../Events/Consumers/Library/LyricDownloadFailureLogger.cs | 2 +- .../Consumers/Library/SubtitleDownloadFailureLogger.cs | 2 +- .../Consumers/Security/AuthenticationFailedLogger.cs | 2 +- .../Consumers/Security/AuthenticationSucceededLogger.cs | 2 +- .../Events/Consumers/Session/PlaybackStartLogger.cs | 2 +- .../Events/Consumers/Session/PlaybackStopLogger.cs | 2 +- .../Events/Consumers/Session/SessionEndedLogger.cs | 2 +- .../Events/Consumers/Session/SessionStartedLogger.cs | 2 +- .../Events/Consumers/System/TaskCompletedLogger.cs | 2 +- .../Consumers/Updates/PluginInstallationFailedLogger.cs | 2 +- .../Events/Consumers/Updates/PluginInstalledLogger.cs | 2 +- .../Events/Consumers/Updates/PluginUninstalledLogger.cs | 2 +- .../Events/Consumers/Updates/PluginUpdatedLogger.cs | 2 +- .../Events/Consumers/Users/UserCreatedLogger.cs | 2 +- .../Events/Consumers/Users/UserDeletedLogger.cs | 2 +- .../Events/Consumers/Users/UserLockedOutLogger.cs | 2 +- .../Events/Consumers/Users/UserPasswordChangedLogger.cs | 2 +- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 4 ++-- Jellyfin.Server.Implementations/Item/ChapterRepository.cs | 2 +- .../Item/MediaAttachmentRepository.cs | 2 +- .../Item/MediaStreamRepository.cs | 2 +- Jellyfin.Server.Implementations/Item/PeopleRepository.cs | 2 +- .../MediaSegments/MediaSegmentManager.cs | 2 +- .../Security/AuthenticationManager.cs | 2 +- .../Trickplay/TrickplayManager.cs | 2 +- .../Users/DefaultAuthenticationProvider.cs | 2 +- .../Users/DefaultPasswordResetProvider.cs | 2 +- Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs | 2 +- .../Users/DisplayPreferencesManager.cs | 2 +- .../Users/InvalidAuthProvider.cs | 2 +- Jellyfin.Server.Implementations/Users/UserManager.cs | 2 +- .../Migrations/Routines/MigrateActivityLogDb.cs | 2 +- .../Migrations/Routines/MigrateAuthenticationDb.cs | 2 +- .../Migrations/Routines/MigrateDisplayPreferencesDb.cs | 2 +- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 7 ++++--- Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs | 2 +- .../Authentication/IAuthenticationProvider.cs | 2 +- .../Authentication/IPasswordResetProvider.cs | 2 +- MediaBrowser.Controller/Channels/Channel.cs | 2 +- MediaBrowser.Controller/Collections/ICollectionManager.cs | 2 +- MediaBrowser.Controller/Devices/IDeviceManager.cs | 4 ++-- MediaBrowser.Controller/Drawing/IImageProcessor.cs | 2 +- MediaBrowser.Controller/Dto/IDtoService.cs | 2 +- MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs | 2 +- MediaBrowser.Controller/Entities/Audio/MusicArtist.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- MediaBrowser.Controller/Entities/CollectionFolder.cs | 2 +- MediaBrowser.Controller/Entities/Folder.cs | 2 +- MediaBrowser.Controller/Entities/InternalItemsQuery.cs | 2 +- MediaBrowser.Controller/Entities/InternalPeopleQuery.cs | 2 +- MediaBrowser.Controller/Entities/Movies/BoxSet.cs | 2 +- MediaBrowser.Controller/Entities/TV/Season.cs | 2 +- MediaBrowser.Controller/Entities/TV/Series.cs | 2 +- MediaBrowser.Controller/Entities/UserRootFolder.cs | 2 +- MediaBrowser.Controller/Entities/UserView.cs | 2 +- MediaBrowser.Controller/Entities/UserViewBuilder.cs | 2 +- MediaBrowser.Controller/IDisplayPreferencesManager.cs | 2 +- MediaBrowser.Controller/Library/IIntroProvider.cs | 3 ++- MediaBrowser.Controller/Library/ILibraryManager.cs | 2 +- MediaBrowser.Controller/Library/IMediaSourceManager.cs | 2 +- MediaBrowser.Controller/Library/IMusicManager.cs | 2 +- MediaBrowser.Controller/Library/IUserDataManager.cs | 2 +- MediaBrowser.Controller/Library/IUserManager.cs | 2 +- .../Library/PlaybackProgressEventArgs.cs | 2 +- MediaBrowser.Controller/LiveTv/ILiveTvManager.cs | 2 +- MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs | 2 +- .../MediaSegments/IMediaSegmentManager.cs | 2 +- MediaBrowser.Controller/Net/AuthorizationInfo.cs | 2 +- MediaBrowser.Controller/Playlists/Playlist.cs | 2 +- MediaBrowser.Controller/Session/ISessionManager.cs | 5 +++-- MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs | 3 ++- MediaBrowser.Controller/Trickplay/ITrickplayManager.cs | 2 +- MediaBrowser.Model/Activity/IActivityManager.cs | 2 +- MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +- MediaBrowser.Model/Library/UserViewQuery.cs | 2 +- MediaBrowser.Model/Querying/LatestItemsQuery.cs | 2 +- MediaBrowser.Model/Querying/NextUpQuery.cs | 2 +- MediaBrowser.Model/Users/UserPolicy.cs | 2 +- .../Entities/AccessSchedule.cs | 2 +- .../Entities/ActivityLog.cs | 2 +- .../Entities/AncestorId.cs | 2 +- .../Entities/AttachmentStreamInfo.cs | 2 +- .../Entities/BaseItemEntity.cs | 2 +- .../Entities/BaseItemExtraType.cs | 2 +- .../Entities/BaseItemImageInfo.cs | 2 +- .../Entities/BaseItemMetadataField.cs | 2 +- .../Entities/BaseItemProvider.cs | 2 +- .../Entities/BaseItemTrailerType.cs | 2 +- .../Jellyfin.Database.Implementations/Entities/Chapter.cs | 2 +- .../Entities/CustomItemDisplayPreferences.cs | 2 +- .../Entities/DisplayPreferences.cs | 2 +- .../Jellyfin.Database.Implementations/Entities/Group.cs | 2 +- .../Entities/HomeSection.cs | 2 +- .../Entities/ImageInfo.cs | 2 +- .../Entities/ImageInfoImageType.cs | 2 +- .../Entities/ItemDisplayPreferences.cs | 2 +- .../Entities/ItemValue.cs | 2 +- .../Entities/ItemValueMap.cs | 2 +- .../Entities/ItemValueType.cs | 2 +- .../Entities/Libraries/Artwork.cs | 2 +- .../Entities/Libraries/Book.cs | 2 +- .../Entities/Libraries/BookMetadata.cs | 2 +- .../Entities/Libraries/Chapter.cs | 2 +- .../Entities/Libraries/Collection.cs | 2 +- .../Entities/Libraries/CollectionItem.cs | 2 +- .../Entities/Libraries/Company.cs | 2 +- .../Entities/Libraries/CompanyMetadata.cs | 2 +- .../Entities/Libraries/CustomItem.cs | 2 +- .../Entities/Libraries/CustomItemMetadata.cs | 2 +- .../Entities/Libraries/Episode.cs | 2 +- .../Entities/Libraries/EpisodeMetadata.cs | 2 +- .../Entities/Libraries/Genre.cs | 2 +- .../Entities/Libraries/ItemMetadata.cs | 2 +- .../Entities/Libraries/Library.cs | 2 +- .../Entities/Libraries/LibraryItem.cs | 2 +- .../Entities/Libraries/MediaFile.cs | 2 +- .../Entities/Libraries/MediaFileStream.cs | 2 +- .../Entities/Libraries/MetadataProvider.cs | 2 +- .../Entities/Libraries/MetadataProviderId.cs | 2 +- .../Entities/Libraries/Movie.cs | 2 +- .../Entities/Libraries/MovieMetadata.cs | 2 +- .../Entities/Libraries/MusicAlbum.cs | 2 +- .../Entities/Libraries/MusicAlbumMetadata.cs | 2 +- .../Entities/Libraries/Person.cs | 2 +- .../Entities/Libraries/PersonRole.cs | 2 +- .../Entities/Libraries/Photo.cs | 2 +- .../Entities/Libraries/PhotoMetadata.cs | 2 +- .../Entities/Libraries/Rating.cs | 2 +- .../Entities/Libraries/RatingSource.cs | 2 +- .../Entities/Libraries/Release.cs | 2 +- .../Entities/Libraries/Season.cs | 2 +- .../Entities/Libraries/SeasonMetadata.cs | 2 +- .../Entities/Libraries/Series.cs | 2 +- .../Entities/Libraries/SeriesMetadata.cs | 2 +- .../Entities/Libraries/Track.cs | 2 +- .../Entities/Libraries/TrackMetadata.cs | 2 +- .../Entities/MediaSegment.cs | 2 +- .../Entities/MediaStreamInfo.cs | 2 +- .../Entities/MediaStreamTypeEntity.cs | 2 +- .../Jellyfin.Database.Implementations/Entities/People.cs | 2 +- .../Entities/PeopleBaseItemMap.cs | 2 +- .../Entities/Permission.cs | 2 +- .../Entities/Preference.cs | 2 +- .../Entities/ProgramAudioEntity.cs | 2 +- .../Entities/Security/ApiKey.cs | 2 +- .../Entities/Security/Device.cs | 2 +- .../Entities/Security/DeviceOptions.cs | 2 +- .../Entities/TrickplayInfo.cs | 2 +- .../Jellyfin.Database.Implementations/Entities/User.cs | 2 +- .../Jellyfin.Database.Implementations/Entities/UserData.cs | 2 +- .../Enums/MediaSegmentType.cs | 2 +- .../Interfaces/IHasArtwork.cs | 2 +- .../Interfaces/IHasCompanies.cs | 2 +- .../Interfaces/IHasPermissions.cs | 2 +- .../Interfaces/IHasReleases.cs | 2 +- .../Jellyfin.Database.Implementations/JellyfinDbContext.cs | 4 ++-- .../ModelConfiguration/ActivityLogConfiguration.cs | 2 +- .../ModelConfiguration/AncestorIdConfiguration.cs | 2 +- .../ModelConfiguration/ApiKeyConfiguration.cs | 2 +- .../AttachmentStreamInfoConfiguration.cs | 2 +- .../ModelConfiguration/BaseItemConfiguration.cs | 2 +- .../BaseItemMetadataFieldConfiguration.cs | 2 +- .../ModelConfiguration/BaseItemProviderConfiguration.cs | 2 +- .../ModelConfiguration/BaseItemTrailerTypeConfiguration.cs | 2 +- .../ModelConfiguration/ChapterConfiguration.cs | 2 +- .../CustomItemDisplayPreferencesConfiguration.cs | 2 +- .../ModelConfiguration/DeviceConfiguration.cs | 2 +- .../ModelConfiguration/DeviceOptionsConfiguration.cs | 2 +- .../ModelConfiguration/DisplayPreferencesConfiguration.cs | 2 +- .../ModelConfiguration/ItemValuesConfiguration.cs | 2 +- .../ModelConfiguration/ItemValuesMapConfiguration.cs | 2 +- .../ModelConfiguration/MediaStreamInfoConfiguration.cs | 2 +- .../ModelConfiguration/PeopleBaseItemMapConfiguration.cs | 2 +- .../ModelConfiguration/PeopleConfiguration.cs | 2 +- .../ModelConfiguration/PermissionConfiguration.cs | 2 +- .../ModelConfiguration/PreferenceConfiguration.cs | 2 +- .../ModelConfiguration/TrickplayInfoConfiguration.cs | 2 +- .../ModelConfiguration/UserConfiguration.cs | 2 +- .../ModelConfiguration/UserDataConfiguration.cs | 2 +- .../Jellyfin.Database.Providers.Sqlite.csproj | 7 ------- .../Migrations/SqliteDesignTimeJellyfinDbFactory.cs | 3 +-- .../ModelBuilderExtensions.cs | 2 +- .../ValueConverters/DateTimeKindValueConverter.cs | 2 +- src/Jellyfin.Drawing/ImageProcessor.cs | 2 +- src/Jellyfin.LiveTv/Channels/ChannelManager.cs | 2 +- src/Jellyfin.LiveTv/LiveTvManager.cs | 2 +- .../Auth/CustomAuthenticationHandlerTests.cs | 2 +- .../DefaultAuthorizationHandlerTests.cs | 2 +- .../FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs | 2 +- .../IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs | 2 +- .../Jellyfin.Api.Tests/Controllers/UserControllerTests.cs | 2 +- tests/Jellyfin.Api.Tests/TestHelpers.cs | 4 ++-- .../EfMigrations/EfMigrationTests.cs | 1 + .../SessionManager/SessionManagerTests.cs | 2 +- .../Parsers/MovieNfoParserTests.cs | 2 +- 240 files changed, 252 insertions(+), 255 deletions(-) diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 4a0662e16a..60f515f24d 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -4,7 +4,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 356d1e437a..0ce967e6a9 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -5,8 +5,8 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Common; using MediaBrowser.Controller.Channels; diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index fb0a55135f..933cfc8cbe 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -5,8 +5,8 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 846663900b..27f6826685 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -19,8 +19,8 @@ using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks.Tasks; using Emby.Server.Implementations.Sorting; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 8631958365..afe5b14e92 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -14,8 +14,8 @@ using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index ffbf8068f0..28cf695007 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 9253a9a69f..9d81b835ce 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; using System.Linq; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 2a28131519..8b88b904bb 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -6,8 +6,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; -using Jellyfin.Data.Entities; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 22baafbb07..5a9315a92a 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -7,8 +7,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 9e780a49e5..7b0a164414 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -9,8 +9,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs index db3aeaaf31..a5be2b616e 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Playlists; diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 959373fec8..42f7deca15 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -8,11 +8,11 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Entities.Security; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Events; diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs index e1c26d0121..9afc511086 100644 --- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs @@ -2,8 +2,8 @@ #pragma warning disable CS1591 using System; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs index d668c17bfc..4c013a8bd7 100644 --- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs @@ -1,8 +1,8 @@ #nullable disable using System; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs index 622a341b6a..cf77861673 100644 --- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs @@ -1,8 +1,8 @@ #nullable disable #pragma warning disable CS1591 -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs index 2a3e456c2d..e42c8a33a3 100644 --- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs @@ -2,8 +2,8 @@ #pragma warning disable CS1591 -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs index afd8ccf9f3..f54188030b 100644 --- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs @@ -2,8 +2,8 @@ #pragma warning disable CS1591 -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs index 12f88bf4da..dd2149b578 100644 --- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs +++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs @@ -1,7 +1,7 @@ #nullable disable -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs index a7821c0e0e..d47e477938 100644 --- a/Emby.Server.Implementations/SyncPlay/Group.cs +++ b/Emby.Server.Implementations/SyncPlay/Group.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 74db077d8c..ee2e18f735 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 2da4839138..7ba75dc243 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -4,8 +4,8 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 2196616dd9..13064882cc 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using Jellyfin.Api.Helpers; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 1fd57eba9c..dd60d01e0c 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -4,8 +4,8 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index b711990261..e7b7405ca9 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -130,7 +130,7 @@ public class ImageController : BaseJellyfinApiController await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); } - user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); + user.ProfileImage = new Database.Implementations.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); await _providerManager .SaveImage(stream, mimeType, user.ProfileImage.Path) diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index e326b925b8..c4b9767565 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -6,7 +6,7 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index ff4540f58f..bde1758e99 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -11,8 +11,8 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryDtos; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Api; diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 09a7b73b94..363acf815a 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -5,8 +5,8 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 0cb20e433d..1e45e53ca1 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -4,8 +4,8 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index b0c493fbec..4d12dc18fc 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -4,7 +4,7 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index 794c6500c6..1577b45947 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 43c5384dce..52cb87e72c 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -3,8 +3,8 @@ using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 5075d91be7..52982c362d 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -3,8 +3,8 @@ using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 6cc2b4244c..0e04beb14e 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -7,8 +7,8 @@ using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index bbfa270db9..ebf98da456 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -6,8 +6,8 @@ using System.Linq; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 6487160303..ebd0288ca6 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -8,8 +8,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 1801b6bfd4..7b493d3fa0 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -8,8 +8,8 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 3c2691cb5e..e10e940f21 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -5,8 +5,8 @@ using System.Security.Claims; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Data/Events/Users/UserCreatedEventArgs.cs b/Jellyfin.Data/Events/Users/UserCreatedEventArgs.cs index b3b8d28318..8de34fec2c 100644 --- a/Jellyfin.Data/Events/Users/UserCreatedEventArgs.cs +++ b/Jellyfin.Data/Events/Users/UserCreatedEventArgs.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; namespace Jellyfin.Data.Events.Users { diff --git a/Jellyfin.Data/Events/Users/UserDeletedEventArgs.cs b/Jellyfin.Data/Events/Users/UserDeletedEventArgs.cs index d57c917c9f..c85de34ded 100644 --- a/Jellyfin.Data/Events/Users/UserDeletedEventArgs.cs +++ b/Jellyfin.Data/Events/Users/UserDeletedEventArgs.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; namespace Jellyfin.Data.Events.Users { diff --git a/Jellyfin.Data/Events/Users/UserLockedOutEventArgs.cs b/Jellyfin.Data/Events/Users/UserLockedOutEventArgs.cs index 4475948219..46b399d26d 100644 --- a/Jellyfin.Data/Events/Users/UserLockedOutEventArgs.cs +++ b/Jellyfin.Data/Events/Users/UserLockedOutEventArgs.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; namespace Jellyfin.Data.Events.Users { diff --git a/Jellyfin.Data/Events/Users/UserPasswordChangedEventArgs.cs b/Jellyfin.Data/Events/Users/UserPasswordChangedEventArgs.cs index a235ccada9..ee41147d5d 100644 --- a/Jellyfin.Data/Events/Users/UserPasswordChangedEventArgs.cs +++ b/Jellyfin.Data/Events/Users/UserPasswordChangedEventArgs.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; namespace Jellyfin.Data.Events.Users { diff --git a/Jellyfin.Data/Events/Users/UserUpdatedEventArgs.cs b/Jellyfin.Data/Events/Users/UserUpdatedEventArgs.cs index 780ace6abe..0f2763f366 100644 --- a/Jellyfin.Data/Events/Users/UserUpdatedEventArgs.cs +++ b/Jellyfin.Data/Events/Users/UserUpdatedEventArgs.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; namespace Jellyfin.Data.Events.Users { diff --git a/Jellyfin.Data/UserEntityExtensions.cs b/Jellyfin.Data/UserEntityExtensions.cs index 8bf82265c9..149fc9042d 100644 --- a/Jellyfin.Data/UserEntityExtensions.cs +++ b/Jellyfin.Data/UserEntityExtensions.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel; using System.Linq; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Database.Implementations.Interfaces; diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index 007a468bf2..8d492f7cd7 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -1,10 +1,10 @@ using System; using System.Linq; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Querying; using Microsoft.EntityFrameworkCore; diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index e414a8232f..51a1186452 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -5,11 +5,11 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Data; using Jellyfin.Data.Dtos; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Entities.Security; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Library/LyricDownloadFailureLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Library/LyricDownloadFailureLogger.cs index 0d52bb9856..5f4864e953 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Library/LyricDownloadFailureLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Library/LyricDownloadFailureLogger.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Events; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs index 0a8c064a99..8fe380e4f4 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Library/SubtitleDownloadFailureLogger.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Events; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs index a4424c7391..1a8931a6dc 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Authentication; using MediaBrowser.Model.Activity; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs index e0ecef2a5d..584d559e44 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs @@ -1,6 +1,6 @@ using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Authentication; using MediaBrowser.Model.Activity; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs index 0ef929a99b..73323acb37 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs @@ -1,8 +1,8 @@ using System; using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs index 7d452ea2fd..b75567539c 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs @@ -1,8 +1,8 @@ using System; using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs index 77e7859c6f..b90708a2f2 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionEndedLogger.cs @@ -1,6 +1,6 @@ using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Session; using MediaBrowser.Model.Activity; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs index 141dc20ea3..139c2e2acb 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/SessionStartedLogger.cs @@ -1,6 +1,6 @@ using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Session; using MediaBrowser.Model.Activity; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs index b0a9393eb6..da82a3b30f 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedLogger.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Text; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Globalization; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs index 0ae9b7f66f..632f30c7ad 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedLogger.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Events; using MediaBrowser.Model.Activity; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs index 287ba578ba..4b49b714cf 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledLogger.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Model.Activity; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs index 2de207b152..2d24de7fc6 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledLogger.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Model.Activity; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs index 08d6bf9c25..e892d3dd9a 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUpdatedLogger.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Model.Activity; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs index a09c344f61..4f063f6a1b 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserCreatedLogger.cs @@ -1,7 +1,7 @@ using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events.Users; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Globalization; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs index 46da8044a1..ba4a072e84 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedLogger.cs @@ -1,8 +1,8 @@ using System; using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events.Users; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Globalization; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs index 1d0d016a74..bbc00567d1 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserLockedOutLogger.cs @@ -1,7 +1,7 @@ using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events.Users; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Globalization; diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs index 2b8f966a80..7219704ec6 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserPasswordChangedLogger.cs @@ -1,7 +1,7 @@ using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events.Users; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Events; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Globalization; diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 1f04b28290..4f69022441 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -16,9 +16,9 @@ using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; @@ -38,7 +38,7 @@ using MediaBrowser.Model.Querying; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using BaseItemDto = MediaBrowser.Controller.Entities.BaseItem; -using BaseItemEntity = Jellyfin.Data.Entities.BaseItemEntity; +using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity; namespace Jellyfin.Server.Implementations.Item; diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs index 48b94a5f3f..93e15735c9 100644 --- a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs +++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Jellyfin.Data.Entities; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Dto; diff --git a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs index 18167cc530..3ae6dbd702 100644 --- a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; -using Jellyfin.Data.Entities; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using Microsoft.EntityFrameworkCore; diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs index f700718841..36c3b9e565 100644 --- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; -using Jellyfin.Data.Entities; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 01a0ade63c..51040cee73 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index fa507ad040..d6eeafacc3 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -5,8 +5,8 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs index 534e80f4e7..cf0293463f 100644 --- a/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs +++ b/Jellyfin.Server.Implementations/Security/AuthenticationManager.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Jellyfin.Data.Entities.Security; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities.Security; using MediaBrowser.Controller.Security; using Microsoft.EntityFrameworkCore; diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index b55d2271a6..bf39f13a77 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -7,8 +7,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; -using Jellyfin.Data.Entities; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; diff --git a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs index acada7aa46..35c43b176d 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs @@ -2,7 +2,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Authentication; using MediaBrowser.Model.Cryptography; using Microsoft.Extensions.Logging; diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs index cefbd0624d..6296881a9e 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -4,7 +4,7 @@ using System.IO; using System.Security.Cryptography; using System.Text.Json; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs index 02a52e5f25..92e2bb4fa7 100644 --- a/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs +++ b/Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs @@ -1,9 +1,9 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs index 3f9491038d..0f21e11a35 100644 --- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs +++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs @@ -5,8 +5,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller; using Microsoft.EntityFrameworkCore; diff --git a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs index c4e4c460a6..caf9d5bd9a 100644 --- a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs +++ b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Authentication; namespace Jellyfin.Server.Implementations.Users diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 0105f8162d..3c39e5503b 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -8,11 +8,11 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Events.Users; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index 933d85de01..e9fe9abceb 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using Emby.Server.Implementations.Data; -using Jellyfin.Data.Entities; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index a50990ac5d..feaf46c843 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -2,8 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using Emby.Server.Implementations.Data; -using Jellyfin.Data.Entities.Security; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities.Security; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using Microsoft.Data.Sqlite; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index 4f6c5100d9..a8fa2e52a1 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -5,8 +5,8 @@ using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using Emby.Server.Implementations.Data; -using Jellyfin.Data.Entities; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 490daae42b..08eea03086 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -11,8 +11,8 @@ using System.Linq; using System.Text; using System.Threading; using Emby.Server.Implementations.Data; -using Jellyfin.Data.Entities; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using Jellyfin.Server.Implementations.Item; using MediaBrowser.Controller; @@ -21,7 +21,8 @@ using MediaBrowser.Model.Entities; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Chapter = Jellyfin.Data.Entities.Chapter; +using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity; +using Chapter = Jellyfin.Database.Implementations.Entities.Chapter; namespace Jellyfin.Server.Migrations.Routines; @@ -125,7 +126,7 @@ public class MigrateLibraryDb : IMigrationRoutine dbContext.ItemValues.ExecuteDelete(); // EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow. - var localItems = new Dictionary<(int Type, string CleanValue), (ItemValue ItemValue, List ItemIds)>(); + var localItems = new Dictionary<(int Type, string CleanValue), (Database.Implementations.Entities.ItemValue ItemValue, List ItemIds)>(); foreach (SqliteDataReader dto in connection.Query(itemValueQuery)) { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index bd5bf98e0f..c40560660d 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -2,8 +2,8 @@ using System; using System.IO; using Emby.Server.Implementations.Data; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions.Json; using Jellyfin.Server.Implementations.Users; diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index 81b532fda8..976a667acd 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Authentication diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs index 8c9d1baf88..592ce99556 100644 --- a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs +++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs @@ -4,7 +4,7 @@ using System; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Authentication diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index f6cbf5a00c..199e22b3fa 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using Jellyfin.Data; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Querying; diff --git a/MediaBrowser.Controller/Collections/ICollectionManager.cs b/MediaBrowser.Controller/Collections/ICollectionManager.cs index 38a78a67b5..206b5ac426 100644 --- a/MediaBrowser.Controller/Collections/ICollectionManager.cs +++ b/MediaBrowser.Controller/Collections/ICollectionManager.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index fe7dc1cf94..ea38950d32 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -1,10 +1,10 @@ using System; using System.Threading.Tasks; using Jellyfin.Data.Dtos; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Entities.Security; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 702ce39a2a..4eeec99b0b 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index 22453f0f76..f1d507fcbd 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -1,7 +1,7 @@ #pragma warning disable CA1002 using System.Collections.Generic; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index d6e6592429..d016d8f62b 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -9,8 +9,8 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 39dc909633..58841e5b78 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -9,8 +9,8 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Providers; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 29481481c4..1dd289631d 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -13,8 +13,8 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index b7b5dac034..ca79e62454 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -11,8 +11,8 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions.Json; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index d3e9da6226..dd85a6ec0e 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -14,8 +14,8 @@ using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using J2N.Collections.Generic.Extensions; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 57a8a01131..5ce5fd4fa9 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index 3e1d892748..203a16a668 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; namespace MediaBrowser.Controller.Entities { diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index a252b7a25f..d656fccb4f 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -8,8 +8,8 @@ using System.Collections.Immutable; using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Querying; diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 9dbac1e920..1293528fba 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -7,8 +7,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json.Serialization; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Common; using MediaBrowser.Controller.Dto; diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 470702f3e6..5dad158519 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -10,8 +10,8 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index 7ae4a4a2cd..bc7e22d9a0 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index f5ca3737c2..dfa31315cb 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -8,8 +8,8 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.TV; diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 18845ab9f6..c2b4da32ac 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -7,8 +7,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Library; diff --git a/MediaBrowser.Controller/IDisplayPreferencesManager.cs b/MediaBrowser.Controller/IDisplayPreferencesManager.cs index 10c0f56e09..a97096eaee 100644 --- a/MediaBrowser.Controller/IDisplayPreferencesManager.cs +++ b/MediaBrowser.Controller/IDisplayPreferencesManager.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; namespace MediaBrowser.Controller { diff --git a/MediaBrowser.Controller/Library/IIntroProvider.cs b/MediaBrowser.Controller/Library/IIntroProvider.cs index 4a9721acbe..860e948afe 100644 --- a/MediaBrowser.Controller/Library/IIntroProvider.cs +++ b/MediaBrowser.Controller/Library/IIntroProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Library @@ -23,6 +24,6 @@ namespace MediaBrowser.Controller.Library /// The item. /// The user. /// IEnumerable{System.String}. - Task> GetIntros(BaseItem item, Jellyfin.Data.Entities.User user); + Task> GetIntros(BaseItem item, User user); } } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 13915dc5c6..a3e3e4991c 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index eb697268c7..2b6781a197 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -6,7 +6,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; diff --git a/MediaBrowser.Controller/Library/IMusicManager.cs b/MediaBrowser.Controller/Library/IMusicManager.cs index 7ba8fc20cf..20764ec601 100644 --- a/MediaBrowser.Controller/Library/IMusicManager.cs +++ b/MediaBrowser.Controller/Library/IMusicManager.cs @@ -1,7 +1,7 @@ #pragma warning disable CA1002, CS1591 using System.Collections.Generic; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index 5a2deda66a..eb46611dd9 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Threading; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 1c115be857..0109cf4b7d 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Users; diff --git a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs index 76e9eb1f54..b0a6782c77 100644 --- a/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs +++ b/MediaBrowser.Controller/Library/PlaybackProgressEventArgs.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index c0e46ba245..8d59eef9f1 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -6,8 +6,8 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index caa312987d..7586ac9024 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -6,8 +6,8 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs index 1e75446e1c..456977b88e 100644 --- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentManager.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.MediaSegments; diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs index e452f26494..dd5eb9a01d 100644 --- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs +++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs @@ -1,6 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; namespace MediaBrowser.Controller.Net { diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index 53e04066f0..1062399e3f 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -10,8 +10,8 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 5dd0413b4d..21131e6b5e 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -6,7 +6,8 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities.Security; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Entities.Security; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; @@ -73,7 +74,7 @@ namespace MediaBrowser.Controller.Session /// The remote end point. /// The user. /// A task containing the session information. - Task LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, Jellyfin.Data.Entities.User user); + Task LogSessionActivity(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user); /// /// Used to report that a session controller has connected. diff --git a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs index 66a0c52547..97f653edf5 100644 --- a/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs +++ b/MediaBrowser.Controller/Sorting/IUserBaseItemComparer.cs @@ -1,5 +1,6 @@ #nullable disable +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Library; namespace MediaBrowser.Controller.Sorting @@ -13,7 +14,7 @@ namespace MediaBrowser.Controller.Sorting /// Gets or sets the user. /// /// The user. - Jellyfin.Data.Entities.User User { get; set; } + User User { get; set; } /// /// Gets or sets the user manager. diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs index 800317800d..9ac8ead11c 100644 --- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs +++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs index 28073fb8d7..95aa567ada 100644 --- a/MediaBrowser.Model/Activity/IActivityManager.cs +++ b/MediaBrowser.Model/Activity/IActivityManager.cs @@ -2,9 +2,9 @@ using System; using System.Threading.Tasks; -using Jellyfin.Data.Entities; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Querying; namespace MediaBrowser.Model.Activity diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 7e8949e1fb..7bfd8ca29c 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Library; diff --git a/MediaBrowser.Model/Library/UserViewQuery.cs b/MediaBrowser.Model/Library/UserViewQuery.cs index 643a1f9b13..01d5e3b6ca 100644 --- a/MediaBrowser.Model/Library/UserViewQuery.cs +++ b/MediaBrowser.Model/Library/UserViewQuery.cs @@ -1,8 +1,8 @@ #pragma warning disable CS1591 using System; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; namespace MediaBrowser.Model.Library { diff --git a/MediaBrowser.Model/Querying/LatestItemsQuery.cs b/MediaBrowser.Model/Querying/LatestItemsQuery.cs index 251ff5d681..40dc813978 100644 --- a/MediaBrowser.Model/Querying/LatestItemsQuery.cs +++ b/MediaBrowser.Model/Querying/LatestItemsQuery.cs @@ -2,8 +2,8 @@ #pragma warning disable CS1591 using System; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Querying diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index aee720aa7b..a2a3a9d1bb 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -1,7 +1,7 @@ #pragma warning disable CS1591 using System; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Querying; diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index ba0eaf21cf..3d430e1015 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -7,7 +7,7 @@ using System.ComponentModel.DataAnnotations; using System.Xml.Serialization; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Enums; -using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule; +using AccessSchedule = Jellyfin.Database.Implementations.Entities.AccessSchedule; namespace MediaBrowser.Model.Users { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs index 909e8750f8..e23ac86aaa 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Xml.Serialization; using Jellyfin.Database.Implementations.Enums; -namespace Jellyfin.Data.Entities +namespace Jellyfin.Database.Implementations.Entities { /// /// An entity representing a user's access schedule. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs index 3a76784052..bf623be7ed 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; using Microsoft.Extensions.Logging; -namespace Jellyfin.Data.Entities +namespace Jellyfin.Database.Implementations.Entities { /// /// An entity referencing an activity log entry. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs index 954416dfe1..3d25ae4f45 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs @@ -1,6 +1,6 @@ using System; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Represents the relational information for an . diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs index 19265a0115..aab3082b37 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs @@ -1,6 +1,6 @@ using System; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Provides information about an Attachment to an . diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs index 42be5b3b99..fc9695a091 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; public class BaseItemEntity { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs index 54aef50e40..46a59f7903 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs @@ -1,5 +1,5 @@ #pragma warning disable CS1591 -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; public enum BaseItemExtraType { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs index ac6b72acea..71d60fc25d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs @@ -2,7 +2,7 @@ using System; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Enum TrailerTypes. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs index 27bbfc4731..e7dbc8e9fd 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs @@ -1,6 +1,6 @@ using System; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Enum MetadataFields. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs index c0c5e3147f..73bb583e31 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs @@ -1,6 +1,6 @@ using System; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Represents a Key-Value relation of an BaseItem's provider. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs index 2bb648138a..db329ad2af 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs @@ -1,6 +1,6 @@ using System; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Enum TrailerTypes. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs index 579442cdb6..f9b9813281 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs @@ -1,6 +1,6 @@ using System; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// The Chapter entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs index a606595122..b3d4b16bc6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs @@ -2,7 +2,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace Jellyfin.Data.Entities +namespace Jellyfin.Database.Implementations.Entities { /// /// An entity that represents a user's custom display preferences for a specific item. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs index 82bf007a8a..ae6966e594 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Enums; -namespace Jellyfin.Data.Entities +namespace Jellyfin.Database.Implementations.Entities { /// /// An entity representing a user's display preferences. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs index 4b343c164a..9dd2486464 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities +namespace Jellyfin.Database.Implementations.Entities { /// /// An entity representing a group. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs index edffec4aba..584550ac56 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Enums; -namespace Jellyfin.Data.Entities +namespace Jellyfin.Database.Implementations.Entities { /// /// An entity representing a section on the user's home page. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs index 935a53a263..9c0b36852e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs @@ -2,7 +2,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace Jellyfin.Data.Entities +namespace Jellyfin.Database.Implementations.Entities { /// /// An entity representing an image. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs index f78178dd22..6052a95bb2 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Enum ImageType. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs index 7e75a200b6..6770531143 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Enums; -namespace Jellyfin.Data.Entities +namespace Jellyfin.Database.Implementations.Entities { /// /// An entity that represents a user's display preferences for a specific item. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs index 11d8e383ee..b5a31921d8 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Represents an ItemValue for a BaseItem. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs index e80a9aec34..23f6e0f7bc 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs @@ -1,6 +1,6 @@ using System; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Mapping table for the ItemValue BaseItem relation. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs index 3bae3beccd..9e2e11c006 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs @@ -1,5 +1,5 @@ #pragma warning disable CA1027 // Mark enums with FlagsAttribute -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Provides the Value types for an . diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs index b529da8fa7..f3083a96ba 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing artwork. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs index 54c30d92ce..b56c1f9403 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a book. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs index 7b1a68bb55..a284d563a3 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity containing metadata for a book. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs index cbcb9a5f54..eac973060e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a chapter. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs index 8da9793f9d..03b68317a6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a collection. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs index 4bd99d83a7..3777c705b8 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a collection item. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs index 5dc1039a1f..c686751ab7 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a company. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs index a29f08c7f6..fdf1f274fb 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity holding metadata for a . diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs index 92307afecd..70e47d6bc6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a custom item. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs index af2393870f..660e535e3a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity containing metadata for a custom item. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs index 6379755264..7cb71f06d9 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing an episode. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs index b0ef11e0f2..b5c2c3c2a3 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity containing metadata for an . diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs index 329b8973f5..442dced2f4 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a genre. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs index 401d58ff22..e5cbab7e45 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An abstract class that holds metadata. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs index 17673cb1d9..d1877ef438 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a library. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs index 975614be10..4fccf6d731 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a library item. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs index 0913f95be1..6e435579c7 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a file on disk. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs index 3170653fec..4552386fee 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a stream in a media file. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs index afaebb8e86..dc8f153500 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a metadata provider. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs index fa36e58db7..b7c9313a26 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a unique identifier for a metadata provider. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs index beae325ec5..afc0e0f435 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a movie. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs index df48ebf279..3d797d97e7 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity holding the metadata for a movie. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs index d6231bbf02..51f77ce0be 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a music album. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs index 691f3504fa..bfb94c44d6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity holding the metadata for a music album. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs index d9609f1cc0..25cdfdc2ee 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a person. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs index 627f74140b..e1c2113908 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a person's role in media. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs index 094d57139c..b113170e1d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a photo. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs index 6c284307d7..6fae4a024f 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity that holds metadata for a photo. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs index 6b792ffb48..627575024e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a rating for an entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs index 91ee8caa16..8322855991 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// This is the entity to store review ratings, not age ratings. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs index 40466def1d..db148338e2 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a release for a library item, eg. Director's cut vs. standard. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs index fc110b49da..dc9f695d98 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a season. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs index da40a075f5..af1e9fa2b7 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity that holds metadata for seasons. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs index ab484c96d6..1e1633248f 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a series. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs index 28da91e472..b1b2b10bea 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing series metadata. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs index 6d6a920225..f0bd88963a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity representing a track. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs index 042d2b90db..d9b4736a7c 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Data.Entities.Libraries +namespace Jellyfin.Database.Implementations.Entities.Libraries { /// /// An entity holding metadata for a track. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs index 8c1c071e63..c34369d889 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs @@ -2,7 +2,7 @@ using System; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Enums; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// An entity representing the metadata for a group of trickplay tiles. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs index b16b62b104..207317376d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs @@ -2,7 +2,7 @@ using System; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; public class MediaStreamInfo { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs index f57672a2cf..33dd81bdd6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Enum MediaStreamType. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs index 18c778b17a..20cf3e2d9e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// People entity. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs index bfaaf82151..c719a185c4 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs @@ -1,6 +1,6 @@ using System; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Mapping table for People to BaseItems. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs index c488b90e1c..84b86574cc 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs @@ -6,7 +6,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities +namespace Jellyfin.Database.Implementations.Entities { /// /// An entity representing whether the associated user has a specific permission. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs index f4f9dd17ac..c02ea7375a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities +namespace Jellyfin.Database.Implementations.Entities { /// /// An entity representing a preference attached to a user or group. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs index 9d79e5ddb1..cb7255c19f 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Lists types of Audio. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs index 1fcbe0f5e9..25a1d5ce90 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Globalization; -namespace Jellyfin.Data.Entities.Security +namespace Jellyfin.Database.Implementations.Entities.Security { /// /// An entity representing an API key. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs index 67d7f78eda..b0f9b2d569 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Globalization; -namespace Jellyfin.Data.Entities.Security +namespace Jellyfin.Database.Implementations.Entities.Security { /// /// An entity representing a device. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs index 531f66c627..8ac3e364c4 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations.Schema; -namespace Jellyfin.Data.Entities.Security +namespace Jellyfin.Database.Implementations.Entities.Security { /// /// An entity representing custom options for a device. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs index ff9a68beff..06b290e4f2 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs @@ -1,7 +1,7 @@ using System; using System.Text.Json.Serialization; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// An entity representing the metadata for a group of trickplay tiles. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs index aafa92b4a6..31538b5bf3 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs @@ -6,7 +6,7 @@ using System.Text.Json.Serialization; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Database.Implementations.Interfaces; -namespace Jellyfin.Data.Entities +namespace Jellyfin.Database.Implementations.Entities { /// /// An entity representing a user. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs index ced12b9e62..cd80686615 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs @@ -1,6 +1,6 @@ using System; -namespace Jellyfin.Data.Entities; +namespace Jellyfin.Database.Implementations.Entities; /// /// Provides and related data. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs index fed092b978..a6e8732fff 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; namespace Jellyfin.Database.Implementations.Enums; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs index 03c2ca4a47..46007472a2 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Jellyfin.Data.Entities.Libraries; +using Jellyfin.Database.Implementations.Entities.Libraries; namespace Jellyfin.Database.Implementations.Interfaces { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs index ed449a8f14..5cfefa456a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Jellyfin.Data.Entities.Libraries; +using Jellyfin.Database.Implementations.Entities.Libraries; namespace Jellyfin.Database.Implementations.Interfaces { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs index 606b1169b8..99b29e6d35 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; namespace Jellyfin.Database.Implementations.Interfaces; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs index 653572b6e9..742a6a3867 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Jellyfin.Data.Entities.Libraries; +using Jellyfin.Database.Implementations.Entities.Libraries; namespace Jellyfin.Database.Implementations.Interfaces; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs index 5cdc853319..9db70263d2 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs @@ -1,7 +1,7 @@ using System; using System.Linq; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Entities.Security; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Entities.Security; using Jellyfin.Database.Implementations.Interfaces; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs index be99b46d56..a209c5b90d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs index 146df5546c..1cb4a1eb1d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs index 7c15f064d8..ea382c7183 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities.Security; +using Jellyfin.Database.Implementations.Entities.Security; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs index b5fae4053c..66cafc83cf 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs index 67a071039f..37816faece 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs index c101e0085e..a602ea65f4 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs index 175a82e091..dd28000ba6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs index c7efef2390..2a888b7de8 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs index 5935b632a7..d97a39f4d2 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs index 550fa5073b..e8a510ab97 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs index 5f87c8d403..3551f76863 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities.Security; +using Jellyfin.Database.Implementations.Entities.Security; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs index 923a53acaf..9055e80253 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities.Security; +using Jellyfin.Database.Implementations.Entities.Security; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs index 92068c8f29..45e0c64824 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs index 0e63c130e3..c8e003eaa1 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs index 3860b0e161..42ef235326 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs index 37e15f5e72..075af2c053 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs index 125b9e92cd..5e3ab44433 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs index fe25d3064d..e8f77a8067 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs index 90a3ec5d00..d2aed54eb1 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs index d6d3fadfc4..207051bcd1 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs index a198c80f7e..1b364a05ed 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs index 4e830801ee..61b5e06e8a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs index a7e223c435..47604d3217 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs @@ -1,4 +1,4 @@ -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj index 4e5f63f6fe..ac62ea59a1 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj @@ -23,13 +23,6 @@ - - - - - - - diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs index 1629c732c6..78815c3118 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/SqliteDesignTimeJellyfinDbFactory.cs @@ -1,10 +1,9 @@ using Jellyfin.Database.Implementations; -using Jellyfin.Database.Providers.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Logging.Abstractions; -namespace Jellyfin.Server.Implementations.Migrations +namespace Jellyfin.Database.Providers.Sqlite.Migrations { /// /// The design time factory for . diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ModelBuilderExtensions.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ModelBuilderExtensions.cs index 0d75686198..41375874d9 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ModelBuilderExtensions.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ModelBuilderExtensions.cs @@ -1,5 +1,5 @@ using System; -using Jellyfin.Server.Implementations.ValueConverters; +using Jellyfin.Database.Providers.Sqlite.ValueConverters; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ValueConverters/DateTimeKindValueConverter.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ValueConverters/DateTimeKindValueConverter.cs index 2e585c92df..d4a9407b0d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ValueConverters/DateTimeKindValueConverter.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/ValueConverters/DateTimeKindValueConverter.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace Jellyfin.Server.Implementations.ValueConverters +namespace Jellyfin.Database.Providers.Sqlite.ValueConverters { /// /// ValueConverter to specify kind. diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs index fcb315b3a9..7718f6c6a5 100644 --- a/src/Jellyfin.Drawing/ImageProcessor.cs +++ b/src/Jellyfin.Drawing/ImageProcessor.cs @@ -9,7 +9,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs index 402a3f3b0e..0ca294a289 100644 --- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs +++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs @@ -9,8 +9,8 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; diff --git a/src/Jellyfin.LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs index 7ebcc48834..40adb51a58 100644 --- a/src/Jellyfin.LiveTv/LiveTvManager.cs +++ b/src/Jellyfin.LiveTv/LiveTvManager.cs @@ -9,9 +9,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.LiveTv.Configuration; using MediaBrowser.Common.Extensions; diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index 0a6489d0ac..7e44b062cf 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -7,7 +7,7 @@ using AutoFixture.AutoMoq; using Jellyfin.Api.Auth; using Jellyfin.Api.Constants; using Jellyfin.Data; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; diff --git a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs index 162a022f5b..bfc7016d2a 100644 --- a/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandlerTests.cs @@ -7,7 +7,7 @@ using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Constants; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Server.Implementations.Security; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs index 1fe9fc97ef..fc243a8730 100644 --- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandlerTests.cs @@ -7,7 +7,7 @@ using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.FirstTimeSetupPolicy; using Jellyfin.Api.Constants; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs index ed5235252a..6e63c0450b 100644 --- a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs @@ -5,7 +5,7 @@ using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Constants; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Library; diff --git a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs index c7331c7181..a74dab5f29 100644 --- a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; using AutoFixture.Xunit2; using Jellyfin.Api.Controllers; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; diff --git a/tests/Jellyfin.Api.Tests/TestHelpers.cs b/tests/Jellyfin.Api.Tests/TestHelpers.cs index 03884d7754..eff14e5f15 100644 --- a/tests/Jellyfin.Api.Tests/TestHelpers.cs +++ b/tests/Jellyfin.Api.Tests/TestHelpers.cs @@ -5,7 +5,7 @@ using System.Net; using System.Security.Claims; using Jellyfin.Api.Constants; using Jellyfin.Data; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Common.Configuration; @@ -13,7 +13,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Http; using Moq; -using AccessSchedule = Jellyfin.Data.Entities.AccessSchedule; +using AccessSchedule = Jellyfin.Database.Implementations.Entities.AccessSchedule; namespace Jellyfin.Api.Tests { diff --git a/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs b/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs index 3fe2caca79..ba3abd5a22 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/EfMigrations/EfMigrationTests.cs @@ -1,3 +1,4 @@ +using Jellyfin.Database.Providers.Sqlite.Migrations; using Jellyfin.Server.Implementations.Migrations; using Microsoft.EntityFrameworkCore; using Xunit; diff --git a/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs index 9418edc5d6..a5a67046d1 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using Jellyfin.Data.Entities; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index a71a08d8cd..e422eb9b8b 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -1,8 +1,8 @@ using System; using System.Linq; using System.Threading; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; From bfff1b9be276ec33609b2bbf9f509f6a6bb08bd4 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Tue, 25 Mar 2025 16:55:26 +0100 Subject: [PATCH 117/508] Fix reference --- .../Jellyfin.Database.Providers.Sqlite.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj index ac62ea59a1..6899a35f63 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj @@ -27,4 +27,9 @@ + + + + + From 1b388d729682435b92cb10eba67a1170ecbfcc6c Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Tue, 25 Mar 2025 21:25:27 -0600 Subject: [PATCH 118/508] Clean up csproj --- .../Jellyfin.Database.Providers.Sqlite.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj index 6899a35f63..03e5fc4958 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj @@ -23,10 +23,6 @@ - - - - From 08dbb5c842e77f8a3e610fd8e339e4be570a1825 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Mar 2025 06:02:58 -0600 Subject: [PATCH 119/508] Update CI dependencies (#13766) --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- .github/workflows/commands.yml | 2 +- .github/workflows/issue-template-check.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 6a3d4d3514..9faab47db9 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 + uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 + uses: github/codeql-action/autobuild@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 + uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 082084ed4f..5ec4d164a5 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -44,7 +44,7 @@ jobs: with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml index e3e8019568..8a21ab0151 100644 --- a/.github/workflows/issue-template-check.yml +++ b/.github/workflows/issue-template-check.yml @@ -14,7 +14,7 @@ jobs: with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: python-version: '3.13' cache: 'pip' From 296b17bf44d39c116ad7c70aba8f8c144335fe24 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 27 Mar 2025 03:23:36 +0100 Subject: [PATCH 120/508] Feature/backup on migration (#13754) * Added generalised backup for migrations * Added backup strategy to MigrateLibraryDb * Added missing namespace * Fix merge issues * Fixed style issue * change fast backup key to timestamp * Update src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs * Update Fields * applied review comments --- .../Migrations/IDatabaseMigrationRoutine.cs | 12 +++ .../Migrations/IMigrationRoutine.cs | 2 + Jellyfin.Server/Migrations/MigrationRunner.cs | 79 +++++++++++++------ .../Migrations/Routines/MigrateLibraryDb.cs | 2 +- Jellyfin.Server/Program.cs | 4 +- .../IJellyfinDatabaseProvider.cs | 17 ++++ .../SqliteDatabaseProvider.cs | 34 ++++++++ 7 files changed, 123 insertions(+), 27 deletions(-) create mode 100644 Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs diff --git a/Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs b/Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs new file mode 100644 index 0000000000..78ff1e3fd0 --- /dev/null +++ b/Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs @@ -0,0 +1,12 @@ +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Migrations; + +/// +/// Defines a migration that operates on the Database. +/// +internal interface IDatabaseMigrationRoutine : IMigrationRoutine +{ +} diff --git a/Jellyfin.Server/Migrations/IMigrationRoutine.cs b/Jellyfin.Server/Migrations/IMigrationRoutine.cs index c1000eeded..29f681df52 100644 --- a/Jellyfin.Server/Migrations/IMigrationRoutine.cs +++ b/Jellyfin.Server/Migrations/IMigrationRoutine.cs @@ -1,4 +1,6 @@ using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore.Internal; namespace Jellyfin.Server.Migrations { diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index fa799ae6e5..fd540c9c0d 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -2,10 +2,15 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Emby.Server.Implementations; using Emby.Server.Implementations.Serialization; +using Jellyfin.Database.Implementations; +using Jellyfin.Server.Implementations; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Configuration; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -57,7 +62,8 @@ namespace Jellyfin.Server.Migrations /// /// CoreAppHost that hosts current version. /// Factory for making the logger. - public static void Run(CoreAppHost host, ILoggerFactory loggerFactory) + /// A representing the asynchronous operation. + public static async Task Run(CoreAppHost host, ILoggerFactory loggerFactory) { var logger = loggerFactory.CreateLogger(); var migrations = _migrationTypes @@ -67,7 +73,8 @@ namespace Jellyfin.Server.Migrations var migrationOptions = host.ConfigurationManager.GetConfiguration(MigrationsListStore.StoreKey); HandleStartupWizardCondition(migrations, migrationOptions, host.ConfigurationManager.Configuration.IsStartupWizardCompleted, logger); - PerformMigrations(migrations, migrationOptions, options => host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, options), logger); + await PerformMigrations(migrations, migrationOptions, options => host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, options), logger, host.ServiceProvider.GetRequiredService()) + .ConfigureAwait(false); } /// @@ -75,7 +82,8 @@ namespace Jellyfin.Server.Migrations /// /// Application paths. /// Factory for making the logger. - public static void RunPreStartup(ServerApplicationPaths appPaths, ILoggerFactory loggerFactory) + /// A representing the asynchronous operation. + public static async Task RunPreStartup(ServerApplicationPaths appPaths, ILoggerFactory loggerFactory) { var logger = loggerFactory.CreateLogger(); var migrations = _preStartupMigrationTypes @@ -95,7 +103,7 @@ namespace Jellyfin.Server.Migrations : new ServerConfiguration(); HandleStartupWizardCondition(migrations, migrationOptions, serverConfig.IsStartupWizardCompleted, logger); - PerformMigrations(migrations, migrationOptions, options => xmlSerializer.SerializeToFile(options, migrationConfigPath), logger); + await PerformMigrations(migrations, migrationOptions, options => xmlSerializer.SerializeToFile(options, migrationConfigPath), logger, null).ConfigureAwait(false); } private static void HandleStartupWizardCondition(IEnumerable migrations, MigrationOptions migrationOptions, bool isStartWizardCompleted, ILogger logger) @@ -111,38 +119,61 @@ namespace Jellyfin.Server.Migrations migrationOptions.Applied.AddRange(onlyOldInstalls.Select(m => (m.Id, m.Name))); } - private static void PerformMigrations(IMigrationRoutine[] migrations, MigrationOptions migrationOptions, Action saveConfiguration, ILogger logger) + private static async Task PerformMigrations( + IMigrationRoutine[] migrations, + MigrationOptions migrationOptions, + Action saveConfiguration, + ILogger logger, + IJellyfinDatabaseProvider? jellyfinDatabaseProvider) { // save already applied migrations, and skip them thereafter saveConfiguration(migrationOptions); var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet(); + var migrationsToBeApplied = migrations.Where(e => !appliedMigrationIds.Contains(e.Id)).ToArray(); - for (var i = 0; i < migrations.Length; i++) + string? migrationKey = null; + if (jellyfinDatabaseProvider is not null && migrationsToBeApplied.Any(f => f is IDatabaseMigrationRoutine)) { - var migrationRoutine = migrations[i]; - if (appliedMigrationIds.Contains(migrationRoutine.Id)) - { - logger.LogDebug("Skipping migration '{Name}' since it is already applied", migrationRoutine.Name); - continue; - } - - logger.LogInformation("Applying migration '{Name}'", migrationRoutine.Name); - + logger.LogInformation("Performing database backup"); try { - migrationRoutine.Perform(); + migrationKey = await jellyfinDatabaseProvider.MigrationBackupFast(CancellationToken.None).ConfigureAwait(false); + logger.LogInformation("Database backup with key '{BackupKey}' has been successfully created.", migrationKey); } - catch (Exception ex) + catch (NotImplementedException) { - logger.LogError(ex, "Could not apply migration '{Name}'", migrationRoutine.Name); - throw; + logger.LogWarning("Could not perform backup of database before migration because provider does not support it"); } + } - // Mark the migration as completed - logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name); - migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name)); - saveConfiguration(migrationOptions); - logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name); + try + { + foreach (var migrationRoutine in migrationsToBeApplied) + { + logger.LogInformation("Applying migration '{Name}'", migrationRoutine.Name); + + try + { + migrationRoutine.Perform(); + } + catch (Exception ex) + { + logger.LogError(ex, "Could not apply migration '{Name}'", migrationRoutine.Name); + throw; + } + + // Mark the migration as completed + logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name); + migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name)); + saveConfiguration(migrationOptions); + logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name); + } + } + catch (System.Exception) when (migrationKey is not null && jellyfinDatabaseProvider is not null) + { + logger.LogInformation("Rollback on database as migration reported failure."); + await jellyfinDatabaseProvider.RestoreBackupFast(migrationKey, CancellationToken.None).ConfigureAwait(false); + throw; } } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 214a2f4e66..941a276ced 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -29,7 +29,7 @@ namespace Jellyfin.Server.Migrations.Routines; /// /// The migration routine for migrating the userdata database to EF Core. /// -public class MigrateLibraryDb : IMigrationRoutine +internal class MigrateLibraryDb : IDatabaseMigrationRoutine { private const string DbFilename = "library.db"; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 32814393cf..e661d0d4ab 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -121,7 +121,7 @@ namespace Jellyfin.Server } StartupHelpers.PerformStaticInitialization(); - Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory); + await Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory).ConfigureAwait(false); do { @@ -166,7 +166,7 @@ namespace Jellyfin.Server appHost.ServiceProvider = _jellyfinHost.Services; await appHost.InitializeServices(startupConfig).ConfigureAwait(false); - Migrations.MigrationRunner.Run(appHost, _loggerFactory); + await Migrations.MigrationRunner.Run(appHost, _loggerFactory).ConfigureAwait(false); try { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs index 0740165530..566b521dd0 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -45,4 +46,20 @@ public interface IJellyfinDatabaseProvider /// The token that will be used to abort the operation. /// A representing the asynchronous operation. Task RunShutdownTask(CancellationToken cancellationToken); + + /// + /// Runs a full Database backup that can later be restored to. + /// + /// A cancelation token. + /// A key to identify the backup. + /// May throw an NotImplementException if this operation is not supported for this database. + Task MigrationBackupFast(CancellationToken cancellationToken); + + /// + /// Restores a backup that has been previously created by . + /// + /// The key to the backup from which the current database should be restored from. + /// A cancelation token. + /// A representing the result of the asynchronous operation. + Task RestoreBackupFast(string key, CancellationToken cancellationToken); } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index d9eb0ae7a4..e818c3524a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -16,6 +17,7 @@ namespace Jellyfin.Database.Providers.Sqlite; [JellyfinDatabaseProviderKey("Jellyfin-SQLite")] public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider { + private const string BackupFolderName = "SQLiteBackups"; private readonly IApplicationPaths _applicationPaths; private readonly ILogger _logger; @@ -84,4 +86,36 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider { configurationBuilder.Conventions.Add(_ => new DoNotUseReturningClauseConvention()); } + + /// + public Task MigrationBackupFast(CancellationToken cancellationToken) + { + var key = DateTime.UtcNow.ToString("yyyyMMddhhmmss", CultureInfo.InvariantCulture); + var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db"); + var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName); + if (!Directory.Exists(backupFile)) + { + Directory.CreateDirectory(backupFile); + } + + backupFile = Path.Combine(_applicationPaths.DataPath, $"{key}_jellyfin.db"); + File.Copy(path, backupFile); + return Task.FromResult(key); + } + + /// + public Task RestoreBackupFast(string key, CancellationToken cancellationToken) + { + var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db"); + var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db"); + + if (!File.Exists(backupFile)) + { + _logger.LogCritical("Tried to restore a backup that does not exist."); + return Task.CompletedTask; + } + + File.Copy(backupFile, path, true); + return Task.CompletedTask; + } } From 777e0823ba77bf799745f648933e16aa98c3ba56 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> Date: Thu, 27 Mar 2025 05:24:16 +0300 Subject: [PATCH 121/508] Extract container, video and audio compatibility checks (#12678) * Extract container, video and audio compatibility checks * Extract audio compatibility checks * Extract CheckVideoConditions * Simplify direct audio stream check --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 277 ++++++++++++++--------- 1 file changed, 164 insertions(+), 113 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 1ed493708a..e48411e7a1 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -101,21 +101,16 @@ namespace MediaBrowser.Model.Dlna MediaStream audioStream = item.GetDefaultAudioStream(null); + ArgumentNullException.ThrowIfNull(audioStream); + var directPlayInfo = GetAudioDirectPlayProfile(item, audioStream, options); var directPlayMethod = directPlayInfo.PlayMethod; var transcodeReasons = directPlayInfo.TranscodeReasons; - var inputAudioChannels = audioStream?.Channels; - var inputAudioBitrate = audioStream?.BitRate; - var inputAudioSampleRate = audioStream?.SampleRate; - var inputAudioBitDepth = audioStream?.BitDepth; - if (directPlayMethod is PlayMethod.DirectPlay) { - var profile = options.Profile; - var audioFailureConditions = GetProfileConditionsForAudio(profile.CodecProfiles, item.Container, audioStream?.Codec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, true); - var audioFailureReasons = AggregateFailureConditions(item, profile, "AudioCodecProfile", audioFailureConditions); + var audioFailureReasons = GetCompatibilityAudioCodec(options, item, item.Container, audioStream, null, false, false); transcodeReasons |= audioFailureReasons; if (audioFailureReasons == 0) @@ -188,6 +183,11 @@ namespace MediaBrowser.Model.Dlna SetStreamInfoOptionsFromTranscodingProfile(item, playlistItem, transcodingProfile); + var inputAudioChannels = audioStream.Channels; + var inputAudioBitrate = audioStream.BitRate; + var inputAudioSampleRate = audioStream.SampleRate; + var inputAudioBitDepth = audioStream.BitDepth; + var audioTranscodingConditions = GetProfileConditionsForAudio(options.Profile.CodecProfiles, transcodingProfile.Container, transcodingProfile.AudioCodec, inputAudioChannels, inputAudioBitrate, inputAudioSampleRate, inputAudioBitDepth, false).ToArray(); ApplyTranscodingConditions(playlistItem, audioTranscodingConditions, null, true, true); @@ -810,6 +810,10 @@ namespace MediaBrowser.Model.Dlna MediaStream? audioStream, StreamInfo playlistItem) { + var mediaSource = playlistItem.MediaSource; + + ArgumentNullException.ThrowIfNull(mediaSource); + if (!(item.SupportsTranscoding || item.SupportsDirectStream)) { return (null, null); @@ -824,17 +828,7 @@ namespace MediaBrowser.Model.Dlna } var videoCodec = videoStream?.Codec; - float videoFramerate = videoStream?.ReferenceFrameRate ?? 0; - TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp; - int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); - int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); - var audioCodec = audioStream?.Codec; - var audioProfile = audioStream?.Profile; - var audioChannels = audioStream?.Channels; - var audioBitrate = audioStream?.BitRate; - var audioSampleRate = audioStream?.SampleRate; - var audioBitDepth = audioStream?.BitDepth; var analyzedProfiles = transcodingProfiles .Select(transcodingProfile => @@ -843,24 +837,16 @@ namespace MediaBrowser.Model.Dlna var container = transcodingProfile.Container; - if (options.AllowVideoStreamCopy) + if (videoStream is not null + && options.AllowVideoStreamCopy + && ContainerHelper.ContainsContainer(transcodingProfile.VideoCodec, videoCodec)) { - if (ContainerHelper.ContainsContainer(transcodingProfile.VideoCodec, videoCodec)) - { - var appliedVideoConditions = options.Profile.CodecProfiles - .Where(i => i.Type == CodecType.Video && - i.ContainsAnyCodec(videoCodec, container) && - i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))) - .Select(i => - i.Conditions.All(condition => ConditionProcessor.IsVideoConditionSatisfied(condition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.VideoRangeType, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC))); - - // An empty appliedVideoConditions means that the codec has no conditions for the current video stream - var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied); - rank.Video = conditionsSatisfied ? 1 : 2; - } + var failures = GetCompatibilityVideoCodec(options, mediaSource, container, videoStream); + rank.Video = failures == 0 ? 1 : 2; } - if (options.AllowAudioStreamCopy) + if (audioStream is not null + && options.AllowAudioStreamCopy) { // For Audio stream, we prefer the audio codec that can be directly copied, then the codec that can otherwise satisfies // the transcoding conditions, then the one does not satisfy the transcoding conditions. @@ -870,19 +856,11 @@ namespace MediaBrowser.Model.Dlna foreach (var transcodingAudioCodec in transcodingAudioCodecs) { - var appliedVideoConditions = options.Profile.CodecProfiles - .Where(i => i.Type == CodecType.VideoAudio && - i.ContainsAnyCodec(transcodingAudioCodec, container) && - i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false))) - .Select(i => - i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false))); - - // An empty appliedVideoConditions means that the codec has no conditions for the current audio stream - var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied); + var failures = GetCompatibilityAudioCodec(options, mediaSource, container, audioStream, transcodingAudioCodec, true, false); var rankAudio = 3; - if (conditionsSatisfied) + if (failures == 0) { rankAudio = string.Equals(transcodingAudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ? 1 : 2; } @@ -984,22 +962,12 @@ namespace MediaBrowser.Model.Dlna var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && audioStreamWithSupportedCodec.Channels > (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue); - var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit - && options.Profile.CodecProfiles - .Where(i => i.Type == CodecType.VideoAudio - && i.ContainsAnyCodec(audioStreamWithSupportedCodec.Codec, container) - && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false))) - .Select(i => i.Conditions.All(condition => - { - var satisfied = ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false); - if (!satisfied) - { - playlistItem.TranscodeReasons |= GetTranscodeReasonForFailedCondition(condition); - } + var directAudioFailures = audioStreamWithSupportedCodec is null ? default : GetCompatibilityAudioCodec(options, item, container ?? string.Empty, audioStreamWithSupportedCodec, null, true, false); - return satisfied; - })) - .All(satisfied => satisfied); + playlistItem.TranscodeReasons |= directAudioFailures; + + var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit + && directAudioFailures == 0; directAudioStreamSatisfied = directAudioStreamSatisfied && !playlistItem.TranscodeReasons.HasFlag(TranscodeReason.ContainerBitrateExceedsLimit); @@ -1289,52 +1257,14 @@ namespace MediaBrowser.Model.Dlna DeviceProfile profile = options.Profile; string container = mediaSource.Container; - // Video - int? width = videoStream?.Width; - int? height = videoStream?.Height; - int? bitDepth = videoStream?.BitDepth; - int? videoBitrate = videoStream?.BitRate; - double? videoLevel = videoStream?.Level; - string? videoProfile = videoStream?.Profile; - VideoRangeType? videoRangeType = videoStream?.VideoRangeType; - float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0; - bool? isAnamorphic = videoStream?.IsAnamorphic; - bool? isInterlaced = videoStream?.IsInterlaced; - string? videoCodecTag = videoStream?.CodecTag; - bool? isAvc = videoStream?.IsAVC; - - TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : mediaSource.Timestamp; - int? packetLength = videoStream?.PacketLength; - int? refFrames = videoStream?.RefFrames; - - int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio); - int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video); - - var checkVideoConditions = (ProfileCondition[] conditions) => - conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)); - // Check container conditions - var containerProfileReasons = AggregateFailureConditions( - mediaSource, - profile, - "VideoCodecProfile", - profile.ContainerProfiles - .Where(containerProfile => containerProfile.Type == DlnaProfileType.Video && containerProfile.ContainsContainer(container)) - .SelectMany(containerProfile => checkVideoConditions(containerProfile.Conditions))); + var containerProfileReasons = GetCompatibilityContainer(options, mediaSource, container, videoStream); // Check video conditions - var videoCodecProfileReasons = AggregateFailureConditions( - mediaSource, - profile, - "VideoCodecProfile", - profile.CodecProfiles - .Where(codecProfile => codecProfile.Type == CodecType.Video && - codecProfile.ContainsAnyCodec(videoStream?.Codec, container) && - !checkVideoConditions(codecProfile.ApplyConditions).Any()) - .SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions))); + var videoCodecProfileReasons = videoStream is null ? default : GetCompatibilityVideoCodec(options, mediaSource, container, videoStream); // Check audio candidates profile conditions - var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream)); + var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => GetCompatibilityAudioCodecDirect(options, mediaSource, container, audioStream, true, mediaSource.IsSecondaryAudio(audioStream) ?? false)); TranscodeReason subtitleProfileReasons = 0; if (subtitleStream is not null) @@ -1447,20 +1377,6 @@ namespace MediaBrowser.Model.Dlna return (Profile: null, PlayMethod: null, AudioStreamIndex: null, TranscodeReasons: failureReasons); } - private TranscodeReason CheckVideoAudioStreamDirectPlay(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream) - { - var profile = options.Profile; - var audioFailureConditions = GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioStream.Codec, audioStream.Channels, audioStream.BitRate, audioStream.SampleRate, audioStream.BitDepth, audioStream.Profile, mediaSource.IsSecondaryAudio(audioStream)); - - var audioStreamFailureReasons = AggregateFailureConditions(mediaSource, profile, "VideoAudioCodecProfile", audioFailureConditions); - if (audioStream.IsExternal == true) - { - audioStreamFailureReasons |= TranscodeReason.AudioIsExternal; - } - - return audioStreamFailureReasons; - } - private TranscodeReason AggregateFailureConditions(MediaSourceInfo mediaSource, DeviceProfile profile, string type, IEnumerable conditions) { return conditions.Aggregate(0, (reasons, i) => @@ -2315,5 +2231,140 @@ namespace MediaBrowser.Model.Dlna return index; } + + /// + /// Check the profile conditions. + /// + /// Profile conditions. + /// Media source. + /// Video stream. + /// Failed profile conditions. + private IEnumerable CheckVideoConditions(ProfileCondition[] conditions, MediaSourceInfo mediaSource, MediaStream? videoStream) + { + int? width = videoStream?.Width; + int? height = videoStream?.Height; + int? bitDepth = videoStream?.BitDepth; + int? videoBitrate = videoStream?.BitRate; + double? videoLevel = videoStream?.Level; + string? videoProfile = videoStream?.Profile; + VideoRangeType? videoRangeType = videoStream?.VideoRangeType; + float videoFramerate = videoStream is null ? 0 : videoStream.ReferenceFrameRate ?? 0; + bool? isAnamorphic = videoStream?.IsAnamorphic; + bool? isInterlaced = videoStream?.IsInterlaced; + string? videoCodecTag = videoStream?.CodecTag; + bool? isAvc = videoStream?.IsAVC; + + TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : mediaSource.Timestamp; + int? packetLength = videoStream?.PacketLength; + int? refFrames = videoStream?.RefFrames; + + int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio); + int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video); + + return conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)); + } + + /// + /// Check the compatibility of the container. + /// + /// Media options. + /// Media source. + /// Container. + /// Video stream. + /// Transcode reasons if the container is not fully compatible. + private TranscodeReason GetCompatibilityContainer(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream? videoStream) + { + var profile = options.Profile; + + var failures = AggregateFailureConditions( + mediaSource, + profile, + "VideoCodecProfile", + profile.ContainerProfiles + .Where(containerProfile => containerProfile.Type == DlnaProfileType.Video && containerProfile.ContainsContainer(container)) + .SelectMany(containerProfile => CheckVideoConditions(containerProfile.Conditions, mediaSource, videoStream))); + + return failures; + } + + /// + /// Check the compatibility of the video codec. + /// + /// Media options. + /// Media source. + /// Container. + /// Video stream. + /// Transcode reasons if the video stream is not fully compatible. + private TranscodeReason GetCompatibilityVideoCodec(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream videoStream) + { + var profile = options.Profile; + + string videoCodec = videoStream.Codec; + + var failures = AggregateFailureConditions( + mediaSource, + profile, + "VideoCodecProfile", + profile.CodecProfiles + .Where(codecProfile => codecProfile.Type == CodecType.Video && + codecProfile.ContainsAnyCodec(videoCodec, container) && + !CheckVideoConditions(codecProfile.ApplyConditions, mediaSource, videoStream).Any()) + .SelectMany(codecProfile => CheckVideoConditions(codecProfile.Conditions, mediaSource, videoStream))); + + return failures; + } + + /// + /// Check the compatibility of the audio codec. + /// + /// Media options. + /// Media source. + /// Container. + /// Audio stream. + /// Override audio codec. + /// The media source is video. + /// The audio stream is secondary. + /// Transcode reasons if the audio stream is not fully compatible. + private TranscodeReason GetCompatibilityAudioCodec(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream, string? transcodingAudioCodec, bool isVideo, bool isSecondaryAudio) + { + var profile = options.Profile; + + var audioCodec = transcodingAudioCodec ?? audioStream.Codec; + var audioProfile = audioStream.Profile; + var audioChannels = audioStream.Channels; + var audioBitrate = audioStream.BitRate; + var audioSampleRate = audioStream.SampleRate; + var audioBitDepth = audioStream.BitDepth; + + var audioFailureConditions = isVideo + ? GetProfileConditionsForVideoAudio(profile.CodecProfiles, container, audioCodec, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, isSecondaryAudio) + : GetProfileConditionsForAudio(profile.CodecProfiles, container, audioCodec, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, true); + + var failures = AggregateFailureConditions(mediaSource, profile, "AudioCodecProfile", audioFailureConditions); + + return failures; + } + + /// + /// Check the compatibility of the audio codec for direct playback. + /// + /// Media options. + /// Media source. + /// Container. + /// Audio stream. + /// The media source is video. + /// The audio stream is secondary. + /// Transcode reasons if the audio stream is not fully compatible for direct playback. + private TranscodeReason GetCompatibilityAudioCodecDirect(MediaOptions options, MediaSourceInfo mediaSource, string container, MediaStream audioStream, bool isVideo, bool isSecondaryAudio) + { + var failures = GetCompatibilityAudioCodec(options, mediaSource, container, audioStream, null, isVideo, isSecondaryAudio); + + if (audioStream.IsExternal) + { + failures |= TranscodeReason.AudioIsExternal; + } + + return failures; + } } } From cafb7cd002e4214643dcdb5e5592d6c5a811ca1c Mon Sep 17 00:00:00 2001 From: baka0815 Date: Thu, 27 Mar 2025 03:25:53 +0100 Subject: [PATCH 122/508] Change the order of the iso6392.txt file (#13314) * Change the order of the ISO-639-2 list Now the ISO 639-2/T (terminological) comes first (which is the same as the ISO 639-3 code) and the second column is for the ISO 639-2/B (bibliograpihc) code. The terminological code is derived from the native name for the language while the bibliographic code is more of a "legacy feature" where the code is derived from the English name for the language. The format of the file is now ISO 639-2/T (or ISO 639-3) | ISO 639-2/B (where applicable) | ISO 639-1 (two-letter code) | English name | French name * Sort the ISO list by the first column --- .../Localization/iso6392.txt | 48 +++++++++---------- .../Localization/LocalizationManagerTests.cs | 5 +- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/Emby.Server.Implementations/Localization/iso6392.txt b/Emby.Server.Implementations/Localization/iso6392.txt index b55c0fa330..00c2aee62d 100644 --- a/Emby.Server.Implementations/Localization/iso6392.txt +++ b/Emby.Server.Implementations/Localization/iso6392.txt @@ -10,7 +10,6 @@ afr||af|Afrikaans|afrikaans ain|||Ainu|aïnou aka||ak|Akan|akan akk|||Akkadian|akkadien -alb|sqi|sq|Albanian|albanais ale|||Aleut|aléoute alg|||Algonquian languages|algonquines, langues alt|||Southern Altai|altai du Sud @@ -21,7 +20,6 @@ apa|||Apache languages|apaches, langues ara||ar|Arabic|arabe arc|||Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)|araméen d'empire (700-300 BCE) arg||an|Aragonese|aragonais -arm|hye|hy|Armenian|arménien arn|||Mapudungun; Mapuche|mapudungun; mapuche; mapuce arp|||Arapaho|arapaho art|||Artificial languages|artificielles, langues @@ -41,7 +39,6 @@ bak||ba|Bashkir|bachkir bal|||Baluchi|baloutchi bam||bm|Bambara|bambara ban|||Balinese|balinais -baq|eus|eu|Basque|basque bas|||Basa|basa bat|||Baltic languages|baltes, langues bej|||Beja; Bedawiyet|bedja @@ -56,6 +53,7 @@ bin|||Bini; Edo|bini; edo bis||bi|Bislama|bichlamar bla|||Siksika|blackfoot bnt|||Bantu (Other)|bantoues, autres langues +bod|tib|bo|Tibetan|tibétain bos||bs|Bosnian|bosniaque bra|||Braj|braj bre||br|Breton|breton @@ -63,7 +61,6 @@ btk|||Batak languages|batak, langues bua|||Buriat|bouriate bug|||Buginese|bugi bul||bg|Bulgarian|bulgare -bur|mya|my|Burmese|birman byn|||Blin; Bilin|blin; bilen cad|||Caddo|caddo cai|||Central American Indian languages|amérindiennes de L'Amérique centrale, langues @@ -72,14 +69,11 @@ cat||ca|Catalan; Valencian|catalan; valencien cau|||Caucasian languages|caucasiennes, langues ceb|||Cebuano|cebuano cel|||Celtic languages|celtiques, langues; celtes, langues +ces|cze|cs|Czech|tchèque cha||ch|Chamorro|chamorro chb|||Chibcha|chibcha che||ce|Chechen|tchétchène chg|||Chagatai|djaghataï -chi|zho|zh|Chinese|chinois -chi|zho|ze|Chinese; Bilingual|chinois -chi|zho|zh-tw|Chinese; Traditional|chinois -chi|zho|zh-hk|Chinese; Hong Kong|chinois chk|||Chuukese|chuuk chm|||Mari|mari chn|||Chinook jargon|chinook, jargon @@ -101,13 +95,14 @@ crh|||Crimean Tatar; Crimean Turkish|tatar de Crimé crp|||Creoles and pidgins |créoles et pidgins csb|||Kashubian|kachoube cus|||Cushitic languages|couchitiques, langues -cze|ces|cs|Czech|tchèque +cym|wel|cy|Welsh|gallois dak|||Dakota|dakota dan||da|Danish|danois dar|||Dargwa|dargwa day|||Land Dayak languages|dayak, langues del|||Delaware|delaware den|||Slave (Athapascan)|esclave (athapascan) +deu|ger|de|German|allemand dgr|||Dogrib|dogrib din|||Dinka|dinka div||dv|Divehi; Dhivehi; Maldivian|maldivien @@ -116,28 +111,30 @@ dra|||Dravidian languages|dravidiennes, langues dsb|||Lower Sorbian|bas-sorabe dua|||Duala|douala dum|||Dutch, Middle (ca.1050-1350)|néerlandais moyen (ca. 1050-1350) -dut|nld|nl|Dutch; Flemish|néerlandais; flamand dyu|||Dyula|dioula dzo||dz|Dzongkha|dzongkha efi|||Efik|efik egy|||Egyptian (Ancient)|égyptien eka|||Ekajuk|ekajuk +ell|gre|el|Greek, Modern (1453-)|grec moderne (après 1453) elx|||Elamite|élamite eng||en|English|anglais enm|||English, Middle (1100-1500)|anglais moyen (1100-1500) epo||eo|Esperanto|espéranto est||et|Estonian|estonien +eus|baq|eu|Basque|basque ewe||ee|Ewe|éwé ewo|||Ewondo|éwondo fan|||Fang|fang fao||fo|Faroese|féroïen +fas|per|fa|Persian|persan fat|||Fanti|fanti fij||fj|Fijian|fidjien fil|||Filipino; Pilipino|filipino; pilipino fin||fi|Finnish|finnois fiu|||Finno-Ugrian languages|finno-ougriennes, langues fon|||Fon|fon -fre|fra|fr|French|français +fra|fre|fr|French|français frm|||French, Middle (ca.1400-1600)|français moyen (1400-1600) fro|||French, Old (842-ca.1400)|français ancien (842-ca.1400) frc||fr-ca|French (Canada)|french @@ -150,8 +147,6 @@ gaa|||Ga|ga gay|||Gayo|gayo gba|||Gbaya|gbaya gem|||Germanic languages|germaniques, langues -geo|kat|ka|Georgian|géorgien -ger|deu|de|German|allemand gez|||Geez|guèze gil|||Gilbertese|kiribati gla||gd|Gaelic; Scottish Gaelic|gaélique; gaélique écossais @@ -165,7 +160,6 @@ gor|||Gorontalo|gorontalo got|||Gothic|gothique grb|||Grebo|grebo grc|||Greek, Ancient (to 1453)|grec ancien (jusqu'à 1453) -gre|ell|el|Greek, Modern (1453-)|grec moderne (après 1453) grn||gn|Guarani|guarani gsw|||Swiss German; Alemannic; Alsatian|suisse alémanique; alémanique; alsacien guj||gu|Gujarati|goudjrati @@ -186,9 +180,10 @@ hrv||hr|Croatian|croate hsb|||Upper Sorbian|haut-sorabe hun||hu|Hungarian|hongrois hup|||Hupa|hupa +hye|arm|hy|Armenian|arménien iba|||Iban|iban ibo||ig|Igbo|igbo -ice|isl|is|Icelandic|islandais +isl|ice|is|Icelandic|islandais ido||io|Ido|ido iii||ii|Sichuan Yi; Nuosu|yi de Sichuan ijo|||Ijo languages|ijo, langues @@ -217,6 +212,7 @@ kam|||Kamba|kamba kan||kn|Kannada|kannada kar|||Karen languages|karen, langues kas||ks|Kashmiri|kashmiri +kat|geo|ka|Georgian|géorgien kau||kr|Kanuri|kanouri kaw|||Kawi|kawi kaz||kk|Kazakh|kazakh @@ -263,7 +259,6 @@ lui|||Luiseno|luiseno lun|||Lunda|lunda luo|||Luo (Kenya and Tanzania)|luo (Kenya et Tanzanie) lus|||Lushai|lushai -mac|mkd|mk|Macedonian|macédonien mad|||Madurese|madourais mag|||Magahi|magahi mah||mh|Marshallese|marshall @@ -271,11 +266,9 @@ mai|||Maithili|maithili mak|||Makasar|makassar mal||ml|Malayalam|malayalam man|||Mandingo|mandingue -mao|mri|mi|Maori|maori map|||Austronesian languages|austronésiennes, langues mar||mr|Marathi|marathe mas|||Masai|massaï -may|msa|ms|Malay|malais mdf|||Moksha|moksa mdr|||Mandar|mandar men|||Mende|mendé @@ -283,6 +276,7 @@ mga|||Irish, Middle (900-1200)|irlandais moyen (900-1200) mic|||Mi'kmaq; Micmac|mi'kmaq; micmac min|||Minangkabau|minangkabau mis|||Uncoded languages|langues non codées +mkd|mac|mk|Macedonian|macédonien mkh|||Mon-Khmer languages|môn-khmer, langues mlg||mg|Malagasy|malgache mlt||mt|Maltese|maltais @@ -292,11 +286,14 @@ mno|||Manobo languages|manobo, langues moh|||Mohawk|mohawk mon||mn|Mongolian|mongol mos|||Mossi|moré +mri|mao|mi|Maori|maori +msa|may|ms|Malay|malais mul|||Multiple languages|multilingue mun|||Munda languages|mounda, langues mus|||Creek|muskogee mwl|||Mirandese|mirandais mwr|||Marwari|marvari +mya|bur|my|Burmese|birman myn|||Mayan languages|maya, langues myv|||Erzya|erza nah|||Nahuatl languages|nahuatl, langues @@ -313,6 +310,7 @@ new|||Nepal Bhasa; Newari|nepal bhasa; newari nia|||Nias|nias nic|||Niger-Kordofanian languages|nigéro-kordofaniennes, langues niu|||Niuean|niué +nld|dut|nl|Dutch; Flemish|néerlandais; flamand nno||nn|Norwegian Nynorsk; Nynorsk, Norwegian|norvégien nynorsk; nynorsk, norvégien nob||nb|Bokmål, Norwegian; Norwegian Bokmål|norvégien bokmål nog|||Nogai|nogaï; nogay @@ -343,7 +341,6 @@ pan||pa|Panjabi; Punjabi|pendjabi pap|||Papiamento|papiamento pau|||Palauan|palau peo|||Persian, Old (ca.600-400 B.C.)|perse, vieux (ca. 600-400 av. J.-C.) -per|fas|fa|Persian|persan phi|||Philippine languages|philippines, langues phn|||Phoenician|phénicien pli||pi|Pali|pali @@ -363,7 +360,7 @@ rar|||Rarotongan; Cook Islands Maori|rarotonga; maori des îles Cook roa|||Romance languages|romanes, langues roh||rm|Romansh|romanche rom|||Romany|tsigane -rum|ron|ro|Romanian; Moldavian; Moldovan|roumain; moldave +ron|rum|ro|Romanian; Moldavian; Moldovan|roumain; moldave run||rn|Rundi|rundi rup|||Aromanian; Arumanian; Macedo-Romanian|aroumain; macédo-roumain rus||ru|Russian|russe @@ -376,6 +373,7 @@ sam|||Samaritan Aramaic|samaritain san||sa|Sanskrit|sanskrit sas|||Sasak|sasak sat|||Santali|santal +scc|srp|sr|Serbian|serbe scn|||Sicilian|sicilien sco|||Scots|écossais sel|||Selkup|selkoupe @@ -388,7 +386,7 @@ sin||si|Sinhala; Sinhalese|singhalais sio|||Siouan languages|sioux, langues sit|||Sino-Tibetan languages|sino-tibétaines, langues sla|||Slavic languages|slaves, langues -slo|slk|sk|Slovak|slovaque +slk|slo|sk|Slovak|slovaque slv||sl|Slovenian|slovène sma|||Southern Sami|sami du Sud sme||se|Northern Sami|sami du Nord @@ -406,9 +404,9 @@ son|||Songhai languages|songhai, langues sot||st|Sotho, Southern|sotho du Sud spa||es-mx|Spanish; Latin|espagnol; Latin spa||es|Spanish; Castilian|espagnol; castillan +sqi|alb|sq|Albanian|albanais srd||sc|Sardinian|sarde srn|||Sranan Tongo|sranan tongo -srp|scc|sr|Serbian|serbe srr|||Serer|sérère ssa|||Nilo-Saharan languages|nilo-sahariennes, langues ssw||ss|Swati|swati @@ -431,7 +429,6 @@ tet|||Tetum|tetum tgk||tg|Tajik|tadjik tgl||tl|Tagalog|tagalog tha||th|Thai|thaï -tib|bod|bo|Tibetan|tibétain tig|||Tigre|tigré tir||ti|Tigrinya|tigrigna tiv|||Tiv|tiv @@ -470,7 +467,6 @@ wak|||Wakashan languages|wakashanes, langues wal|||Walamo|walamo war|||Waray|waray was|||Washo|washo -wel|cym|cy|Welsh|gallois wen|||Sorbian languages|sorabes, langues wln||wa|Walloon|wallon wol||wo|Wolof|wolof @@ -486,6 +482,10 @@ zbl|||Blissymbols; Blissymbolics; Bliss|symboles Bliss; Bliss zen|||Zenaga|zenaga zgh|||Standard Moroccan Tamazight|amazighe standard marocain zha||za|Zhuang; Chuang|zhuang; chuang +zho|chi|zh|Chinese|chinois +zho|chi|ze|Chinese; Bilingual|chinois +zho|chi|zh-tw|Chinese; Traditional|chinois +zho|chi|zh-hk|Chinese; Hong Kong|chinois znd|||Zande languages|zandé, langues zul||zu|Zulu|zoulou zun|||Zuni|zuni diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index cc67dbc397..5babc91174 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -44,7 +44,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal)); Assert.NotNull(germany); - Assert.Equal("ger", germany!.ThreeLetterISOLanguageName); + Assert.Equal("deu", germany!.ThreeLetterISOLanguageName); Assert.Equal("German", germany.DisplayName); Assert.Equal("German", germany.Name); Assert.Contains("deu", germany.ThreeLetterISOLanguageNames); @@ -53,6 +53,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization [Theory] [InlineData("de")] + [InlineData("deu")] [InlineData("ger")] [InlineData("german")] public async Task FindLanguageInfo_Valid_Success(string identifier) @@ -66,7 +67,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization var germany = localizationManager.FindLanguageInfo(identifier); Assert.NotNull(germany); - Assert.Equal("ger", germany!.ThreeLetterISOLanguageName); + Assert.Equal("deu", germany!.ThreeLetterISOLanguageName); Assert.Equal("German", germany.DisplayName); Assert.Equal("German", germany.Name); Assert.Contains("deu", germany.ThreeLetterISOLanguageNames); From d06ce1f1e035d4630121f45dcadbdebc95cb07e2 Mon Sep 17 00:00:00 2001 From: JQ <81431263+scampower3@users.noreply.github.com> Date: Thu, 27 Mar 2025 10:26:47 +0800 Subject: [PATCH 123/508] Fix only returning one item from /Item/Latest api. (#12492) * Updated to EFcore * Remove unused using * Dont use DateCreated not from episode type or music type * use TranslateQuery to filter out instead and then do the grouping and retrival of min and max datecreated instead * Album also --- .../Library/LibraryManager.cs | 15 +++++++ .../Library/UserViewManager.cs | 15 +++++++ .../Item/BaseItemRepository.cs | 42 +++++++++++++++++++ .../Library/ILibraryManager.cs | 9 ++++ .../Persistence/IItemRepository.cs | 9 ++++ 5 files changed, 90 insertions(+) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 27f6826685..eb8e310729 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1364,6 +1364,21 @@ namespace Emby.Server.Implementations.Library return _itemRepository.GetItemList(query); } + public IReadOnlyList GetLatestItemList(InternalItemsQuery query, IReadOnlyList parents, CollectionType collectionType) + { + SetTopParentIdsOrAncestors(query, parents); + + if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0) + { + if (query.User is not null) + { + AddUserToQuery(query, query.User); + } + } + + return _itemRepository.GetLatestItemList(query, collectionType); + } + public IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection parents, DateTime dateCutoff) { SetTopParentIdsOrAncestors(query, parents); diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 5a9315a92a..87214c273b 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -372,6 +372,21 @@ namespace Emby.Server.Implementations.Library MediaTypes = mediaTypes }; + if (request.GroupItems) + { + if (parents.OfType().All(i => i.CollectionType == CollectionType.tvshows)) + { + query.Limit = limit; + return _libraryManager.GetLatestItemList(query, parents, CollectionType.tvshows); + } + + if (parents.OfType().All(i => i.CollectionType == CollectionType.music)) + { + query.Limit = limit; + return _libraryManager.GetLatestItemList(query, parents, CollectionType.music); + } + } + return _libraryManager.GetItemList(query, parents); } } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 4f69022441..7b5b6b94d7 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -264,6 +264,48 @@ public sealed class BaseItemRepository return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray(); } + /// + public IReadOnlyList GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType) + { + ArgumentNullException.ThrowIfNull(filter); + PrepareFilterQuery(filter); + + // Early exit if collection type is not tvshows or music + if (collectionType != CollectionType.tvshows && collectionType != CollectionType.music) + { + return Array.Empty(); + } + + using var context = _dbProvider.CreateDbContext(); + + // Subquery to group by SeriesNames/Album and get the max Date Created for each group. + var subquery = PrepareItemQuery(context, filter); + subquery = TranslateQuery(subquery, context, filter); + var subqueryGrouped = subquery.GroupBy(g => collectionType == CollectionType.tvshows ? g.SeriesName : g.Album) + .Select(g => new + { + Key = g.Key, + MaxDateCreated = g.Max(a => a.DateCreated) + }) + .OrderByDescending(g => g.MaxDateCreated) + .Select(g => g); + + if (filter.Limit.HasValue) + { + subqueryGrouped = subqueryGrouped.Take(filter.Limit.Value); + } + + filter.Limit = null; + + var mainquery = PrepareItemQuery(context, filter); + mainquery = TranslateQuery(mainquery, context, filter); + mainquery = mainquery.Where(g => g.DateCreated >= subqueryGrouped.Min(s => s.MaxDateCreated)); + mainquery = ApplyGroupingFilter(mainquery, filter); + mainquery = ApplyQueryPaging(mainquery, filter); + + return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray(); + } + /// public IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff) { diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index a3e3e4991c..df90f546cd 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -567,6 +567,15 @@ namespace MediaBrowser.Controller.Library /// List of items. IReadOnlyList GetItemList(InternalItemsQuery query, List parents); + /// + /// Gets the TVShow/Album items for Latest api. + /// + /// The query to use. + /// Items to use for query. + /// Collection Type. + /// List of items. + IReadOnlyList GetLatestItemList(InternalItemsQuery query, IReadOnlyList parents, CollectionType collectionType); + /// /// Gets the list of series presentation keys for next up. /// diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index f1ed4fe274..e185898bfb 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Threading; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; @@ -59,6 +60,14 @@ public interface IItemRepository /// List<BaseItem>. IReadOnlyList GetItemList(InternalItemsQuery filter); + /// + /// Gets the item list. Used mainly by the Latest api endpoint. + /// + /// The query. + /// Collection Type. + /// List<BaseItem>. + IReadOnlyList GetLatestItemList(InternalItemsQuery filter, CollectionType collectionType); + /// /// Gets the list of series presentation keys for next up. /// From 9e4abb7319da628728decfcf04b82cc509638abd Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 27 Mar 2025 12:34:59 +0100 Subject: [PATCH 124/508] Add override for migration if old library still exists (#13779) --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 941a276ced..8e462015f4 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -325,7 +325,8 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old"); SqliteConnection.ClearAllPools(); - File.Move(libraryDbPath, libraryDbPath + ".old"); + + File.Move(libraryDbPath, libraryDbPath + ".old", true); _logger.LogInformation("Migrating Library db took {0}.", migrationTotalTime); From f6b98d0fafb0f741d5e6ff05249ae52a2421e6ae Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Thu, 27 Mar 2025 16:43:25 +0000 Subject: [PATCH 125/508] Add eac3 as audio name format (#13784) --- Emby.Naming/Common/NamingOptions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 48338daf48..6a662aaf55 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -238,6 +238,7 @@ namespace Emby.Naming.Common ".dsp", ".dts", ".dvf", + ".eac3", ".far", ".flac", ".gdm", From ae4b35da462ad569bdc6f10df1cc8095cb1466e5 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Thu, 27 Mar 2025 16:43:39 +0000 Subject: [PATCH 126/508] Include UnratedType in LibraryDb migration query (#13783) --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 8e462015f4..427f04f9d8 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -95,7 +95,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId, PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, - ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName FROM TypedBaseItems + ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType FROM TypedBaseItems """; dbContext.BaseItems.ExecuteDelete(); @@ -1045,6 +1045,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine entity.CleanName = cleanName; } + if (reader.TryGetString(index++, out var unratedType)) + { + entity.UnratedType = unratedType; + } + var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false); var dataKeys = baseItem.GetUserDataKeys(); userDataKeys.AddRange(dataKeys); From b108a8cfc538795c3e5a120c785fffa512e9abfa Mon Sep 17 00:00:00 2001 From: Tobias Kloy Date: Mon, 10 Feb 2025 23:11:22 +0100 Subject: [PATCH 127/508] Fix IsInMixedFolder not being set for Extras in mixed subfolders --- Emby.Server.Implementations/Library/LibraryManager.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index eb8e310729..6a19a41cd6 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2721,6 +2721,8 @@ namespace Emby.Server.Implementations.Library if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name)) { var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false); + bool subFolderIsMixedFolder = filesInSubFolder.Count() > 1; + foreach (var file in filesInSubFolder) { if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType)) @@ -2728,7 +2730,7 @@ namespace Emby.Server.Implementations.Library continue; } - var extra = GetExtra(file, extraType.Value); + var extra = GetExtra(file, extraType.Value, subFolderIsMixedFolder); if (extra is not null) { yield return extra; @@ -2737,7 +2739,7 @@ namespace Emby.Server.Implementations.Library } else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo, out var extraType)) { - var extra = GetExtra(current, extraType.Value); + var extra = GetExtra(current, extraType.Value, false); if (extra is not null) { yield return extra; @@ -2745,7 +2747,7 @@ namespace Emby.Server.Implementations.Library } } - BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType) + BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType, bool isInMixedFolder) { var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetResolversForExtraType(extraType)); if (extra is not Video && extra is not Audio) @@ -2768,6 +2770,7 @@ namespace Emby.Server.Implementations.Library extra.ParentId = Guid.Empty; extra.OwnerId = owner.Id; + extra.IsInMixedFolder = isInMixedFolder; return extra; } } From 7717d2e26171d9d932cba66a2813b7c0d02869e2 Mon Sep 17 00:00:00 2001 From: Tobias Kloy Date: Mon, 10 Feb 2025 23:33:25 +0100 Subject: [PATCH 128/508] Avoid multiple enumerations --- Emby.Server.Implementations/Library/LibraryManager.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 6a19a41cd6..c6b8501cbc 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2721,9 +2721,11 @@ namespace Emby.Server.Implementations.Library if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name)) { var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false); - bool subFolderIsMixedFolder = filesInSubFolder.Count() > 1; + var filesInSubFolderList = filesInSubFolder.ToList(); - foreach (var file in filesInSubFolder) + bool subFolderIsMixedFolder = filesInSubFolderList.Count > 1; + + foreach (var file in filesInSubFolderList) { if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType)) { From 181a37a8cd512a46dfc5af55c43ef1468d206d75 Mon Sep 17 00:00:00 2001 From: timminator <150205162+timminator@users.noreply.github.com> Date: Fri, 28 Mar 2025 00:59:08 +0100 Subject: [PATCH 129/508] Fix consumer count off by one when closing a browser tab with a livestream that is transcoding (#13220) Rework Implementation Fix review issues Add missing nullorempty check Fix closely related #13721 --- .../Session/SessionManager.cs | 87 +++++++++++++++++-- .../Session/ISessionManager.cs | 8 ++ .../Transcoding/TranscodeManager.cs | 9 +- 3 files changed, 87 insertions(+), 17 deletions(-) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 42f7deca15..ac3e105940 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -64,6 +64,9 @@ namespace Emby.Server.Implementations.Session private readonly ConcurrentDictionary _activeConnections = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary> _activeLiveStreamSessions + = new(StringComparer.OrdinalIgnoreCase); + private Timer _idleTimer; private Timer _inactiveTimer; @@ -311,13 +314,49 @@ namespace Emby.Server.Implementations.Session _activeConnections.TryRemove(key, out _); if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId)) { - await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false); + await CloseLiveStreamIfNeededAsync(session.PlayState.LiveStreamId, session.Id).ConfigureAwait(false); } await OnSessionEnded(session).ConfigureAwait(false); } } + /// + public async Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId) + { + bool liveStreamNeedsToBeClosed = false; + + if (_activeLiveStreamSessions.TryGetValue(liveStreamId, out var activeSessionMappings)) + { + if (activeSessionMappings.TryRemove(sessionIdOrPlaySessionId, out var correspondingId)) + { + if (!string.IsNullOrEmpty(correspondingId)) + { + activeSessionMappings.TryRemove(correspondingId, out _); + } + + liveStreamNeedsToBeClosed = true; + } + + if (activeSessionMappings.IsEmpty) + { + _activeLiveStreamSessions.TryRemove(liveStreamId, out _); + } + } + + if (liveStreamNeedsToBeClosed) + { + try + { + await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error closing live stream"); + } + } + } + /// public async ValueTask ReportSessionEnded(string sessionId) { @@ -737,6 +776,11 @@ namespace Emby.Server.Implementations.Session } } + if (!string.IsNullOrEmpty(info.LiveStreamId)) + { + UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId); + } + var eventArgs = new PlaybackStartEventArgs { Item = libraryItem, @@ -794,6 +838,32 @@ namespace Emby.Server.Implementations.Session return OnPlaybackProgress(info, false); } + private void UpdateLiveStreamActiveSessionMappings(string liveStreamId, string sessionId, string playSessionId) + { + var activeSessionMappings = _activeLiveStreamSessions.GetOrAdd(liveStreamId, _ => new ConcurrentDictionary()); + + if (!string.IsNullOrEmpty(playSessionId)) + { + if (!activeSessionMappings.TryGetValue(sessionId, out var currentPlaySessionId) || currentPlaySessionId != playSessionId) + { + if (!string.IsNullOrEmpty(currentPlaySessionId)) + { + activeSessionMappings.TryRemove(currentPlaySessionId, out _); + } + + activeSessionMappings[sessionId] = playSessionId; + activeSessionMappings[playSessionId] = sessionId; + } + } + else + { + if (!activeSessionMappings.TryGetValue(sessionId, out _)) + { + activeSessionMappings[sessionId] = string.Empty; + } + } + } + /// /// Used to report playback progress for an item. /// @@ -834,6 +904,11 @@ namespace Emby.Server.Implementations.Session } } + if (!string.IsNullOrEmpty(info.LiveStreamId)) + { + UpdateLiveStreamActiveSessionMappings(info.LiveStreamId, info.SessionId, info.PlaySessionId); + } + var eventArgs = new PlaybackProgressEventArgs { Item = libraryItem, @@ -1016,14 +1091,7 @@ namespace Emby.Server.Implementations.Session if (!string.IsNullOrEmpty(info.LiveStreamId)) { - try - { - await _mediaSourceManager.CloseLiveStream(info.LiveStreamId).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error closing live stream"); - } + await CloseLiveStreamIfNeededAsync(info.LiveStreamId, session.Id).ConfigureAwait(false); } var eventArgs = new PlaybackStopEventArgs @@ -2071,6 +2139,7 @@ namespace Emby.Server.Implementations.Session } _activeConnections.Clear(); + _activeLiveStreamSessions.Clear(); } } } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 21131e6b5e..47bcfdb6ef 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -342,5 +342,13 @@ namespace MediaBrowser.Controller.Session Task RevokeUserTokens(Guid userId, string currentAccessToken); Task CloseIfNeededAsync(SessionInfo session); + + /// + /// Used to close the livestream if needed. + /// + /// The livestream id. + /// The session id or playsession id. + /// Task. + Task CloseLiveStreamIfNeededAsync(string liveStreamId, string sessionIdOrPlaySessionId); } } diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs index 85bb862c77..c7f9cf2ccf 100644 --- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs +++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs @@ -242,14 +242,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId)) { - try - { - await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error closing live stream for {Path}", job.Path); - } + await _sessionManager.CloseLiveStreamIfNeededAsync(job.LiveStreamId, job.PlaySessionId).ConfigureAwait(false); } } From a123a2cb22096c558953c73432f254eb120a5839 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Fri, 28 Mar 2025 07:59:32 +0800 Subject: [PATCH 130/508] Fix validation of VAAPI/QSV render node path (#13786) Signed-off-by: nyanmisaka --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index cf76f336c8..8fcbba738f 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -862,9 +862,9 @@ namespace MediaBrowser.Controller.MediaEncoding && _mediaEncoder.EncoderVersion >= _minFFmpegVaapiDeviceVendorId; // Priority: 'renderNodePath' > 'vendorId' > 'kernelDriver' - var driverOpts = string.IsNullOrEmpty(renderNodePath) - ? (haveVendorId ? $",vendor_id={vendorId}" : (string.IsNullOrEmpty(kernelDriver) ? string.Empty : $",kernel_driver={kernelDriver}")) - : renderNodePath; + var driverOpts = File.Exists(renderNodePath) + ? renderNodePath + : (haveVendorId ? $",vendor_id={vendorId}" : (string.IsNullOrEmpty(kernelDriver) ? string.Empty : $",kernel_driver={kernelDriver}")); // 'driver' behaves similarly to env LIBVA_DRIVER_NAME driverOpts += string.IsNullOrEmpty(driver) ? string.Empty : ",driver=" + driver; From 07f07ba6bcd4d5466412d1c9965f06a358690545 Mon Sep 17 00:00:00 2001 From: Jacob Warren <109393233+jjwarrenSEP@users.noreply.github.com> Date: Thu, 27 Mar 2025 20:05:03 -0400 Subject: [PATCH 131/508] Fix Sort by Year Bug (#12101) (#13733) --- .../Item/BaseItemRepository.cs | 46 +-------------- .../Item/OrderMapper.cs | 57 +++++++++++++++++++ .../Item/OrderMapperTests.cs | 35 ++++++++++++ 3 files changed, 94 insertions(+), 44 deletions(-) create mode 100644 Jellyfin.Server.Implementations/Item/OrderMapper.cs create mode 100644 tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 7b5b6b94d7..b0a36b3ae6 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -7,11 +7,8 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Immutable; using System.Globalization; -using System.IO; using System.Linq; -using System.Linq.Expressions; using System.Reflection; using System.Text; using System.Text.Json; @@ -1209,45 +1206,6 @@ public sealed class BaseItemRepository return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type); } - private Expression> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) - { -#pragma warning disable CS8603 // Possible null reference return. - return sortBy switch - { - ItemSortBy.AirTime => e => e.SortName, // TODO - ItemSortBy.Runtime => e => e.RunTimeTicks, - ItemSortBy.Random => e => EF.Functions.Random(), - ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.LastPlayedDate, - ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.PlayCount, - ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.IsFavorite, - ItemSortBy.IsFolder => e => e.IsFolder, - ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played, - ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId == query.User!.Id)!.Played, - ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded, - ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), - ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), - ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), - ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, - // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", - ItemSortBy.SeriesSortName => e => e.SeriesName, - // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", - ItemSortBy.Album => e => e.Album, - ItemSortBy.DateCreated => e => e.DateCreated, - ItemSortBy.PremiereDate => e => e.PremiereDate, - ItemSortBy.StartDate => e => e.StartDate, - ItemSortBy.Name => e => e.Name, - ItemSortBy.CommunityRating => e => e.CommunityRating, - ItemSortBy.ProductionYear => e => e.ProductionYear, - ItemSortBy.CriticRating => e => e.CriticRating, - ItemSortBy.VideoBitRate => e => e.TotalBitrate, - ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber, - ItemSortBy.IndexNumber => e => e.IndexNumber, - _ => e => e.SortName - }; -#pragma warning restore CS8603 // Possible null reference return. - - } - private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query) { if (!query.GroupByPresentationUniqueKey) @@ -1302,7 +1260,7 @@ public sealed class BaseItemRepository var firstOrdering = orderBy.FirstOrDefault(); if (firstOrdering != default) { - var expression = MapOrderByField(firstOrdering.OrderBy, filter); + var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter); if (firstOrdering.SortOrder == SortOrder.Ascending) { orderedQuery = query.OrderBy(expression); @@ -1327,7 +1285,7 @@ public sealed class BaseItemRepository foreach (var item in orderBy.Skip(1)) { - var expression = MapOrderByField(item.OrderBy, filter); + var expression = OrderMapper.MapOrderByField(item.OrderBy, filter); if (item.SortOrder == SortOrder.Ascending) { orderedQuery = orderedQuery!.ThenBy(expression); diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs new file mode 100644 index 0000000000..df2fd08b17 --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Static class for methods which maps types of ordering to their respecting ordering functions. +/// +public static class OrderMapper +{ + /// + /// Creates Func to be executed later with a given BaseItemEntity input for sorting items on query. + /// + /// Item property to sort by. + /// Context Query. + /// Func to be executed later for sorting query. + public static Expression> MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query) + { + return sortBy switch + { + ItemSortBy.AirTime => e => e.SortName, // TODO + ItemSortBy.Runtime => e => e.RunTimeTicks, + ItemSortBy.Random => e => EF.Functions.Random(), + ItemSortBy.DatePlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.LastPlayedDate, + ItemSortBy.PlayCount => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.PlayCount, + ItemSortBy.IsFavoriteOrLiked => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.IsFavorite, + ItemSortBy.IsFolder => e => e.IsFolder, + ItemSortBy.IsPlayed => e => e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, + ItemSortBy.IsUnplayed => e => !e.UserData!.FirstOrDefault(f => f.UserId.Equals(query.User!.Id))!.Played, + ItemSortBy.DateLastContentAdded => e => e.DateLastMediaAdded, + ItemSortBy.Artist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Artist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), + ItemSortBy.AlbumArtist => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.AlbumArtist).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), + ItemSortBy.Studio => e => e.ItemValues!.Where(f => f.ItemValue.Type == ItemValueType.Studios).Select(f => f.ItemValue.CleanValue).FirstOrDefault(), + ItemSortBy.OfficialRating => e => e.InheritedParentalRatingValue, + // ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", + ItemSortBy.SeriesSortName => e => e.SeriesName, + // ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder", + ItemSortBy.Album => e => e.Album, + ItemSortBy.DateCreated => e => e.DateCreated, + ItemSortBy.PremiereDate => e => (e.PremiereDate ?? (e.ProductionYear.HasValue ? DateTime.MinValue.AddYears(e.ProductionYear.Value - 1) : null)), + ItemSortBy.StartDate => e => e.StartDate, + ItemSortBy.Name => e => e.Name, + ItemSortBy.CommunityRating => e => e.CommunityRating, + ItemSortBy.ProductionYear => e => e.ProductionYear, + ItemSortBy.CriticRating => e => e.CriticRating, + ItemSortBy.VideoBitRate => e => e.TotalBitrate, + ItemSortBy.ParentIndexNumber => e => e.ParentIndexNumber, + ItemSortBy.IndexNumber => e => e.IndexNumber, + _ => e => e.SortName + }; + } +} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs new file mode 100644 index 0000000000..a6d6b03400 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs @@ -0,0 +1,35 @@ +using System; +using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; +using Jellyfin.Server.Implementations.Item; +using MediaBrowser.Controller.Entities; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Item; + +public class OrderMapperTests +{ + [Fact] + public void ShouldReturnMappedOrderForSortingByPremierDate() + { + var orderFunc = OrderMapper.MapOrderByField(ItemSortBy.PremiereDate, new InternalItemsQuery()).Compile(); + + var expectedDate = new DateTime(1, 2, 3); + var expectedProductionYearDate = new DateTime(4, 1, 1); + + var entityWithOnlyProductionYear = new BaseItemEntity { Id = Guid.NewGuid(), Type = "Test", ProductionYear = expectedProductionYearDate.Year }; + var entityWithOnlyPremierDate = new BaseItemEntity { Id = Guid.NewGuid(), Type = "Test", PremiereDate = expectedDate }; + var entityWithBothPremierDateAndProductionYear = new BaseItemEntity { Id = Guid.NewGuid(), Type = "Test", PremiereDate = expectedDate, ProductionYear = expectedProductionYearDate.Year }; + var entityWithoutEitherPremierDateOrProductionYear = new BaseItemEntity { Id = Guid.NewGuid(), Type = "Test" }; + + var resultWithOnlyProductionYear = orderFunc(entityWithOnlyProductionYear); + var resultWithOnlyPremierDate = orderFunc(entityWithOnlyPremierDate); + var resultWithBothPremierDateAndProductionYear = orderFunc(entityWithBothPremierDateAndProductionYear); + var resultWithoutEitherPremierDateOrProductionYear = orderFunc(entityWithoutEitherPremierDateOrProductionYear); + + Assert.Equal(resultWithOnlyProductionYear, expectedProductionYearDate); + Assert.Equal(resultWithOnlyPremierDate, expectedDate); + Assert.Equal(resultWithBothPremierDateAndProductionYear, expectedDate); + Assert.Null(resultWithoutEitherPremierDateOrProductionYear); + } +} From 9f7057899745e385ccea062497d0aa787e938339 Mon Sep 17 00:00:00 2001 From: Kevin Jilissen <32480819+Kevinjil@users.noreply.github.com> Date: Fri, 28 Mar 2025 01:06:10 +0100 Subject: [PATCH 132/508] Add channel queries to series (#13356) Currently, the IChannel interface can deliver channel result folders which are interpreted as series and seasons. However, Jellyfin does not query for the contents of these folders when viewing said serie of season. This results in empty series in the API. --- MediaBrowser.Controller/Entities/BaseItem.cs | 3 +- MediaBrowser.Controller/Entities/TV/Season.cs | 16 +++++++++ MediaBrowser.Controller/Entities/TV/Series.cs | 35 ++++++++++++++++++- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 1dd289631d..53c832ff37 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -23,6 +23,7 @@ using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; @@ -1683,7 +1684,7 @@ namespace MediaBrowser.Controller.Entities public virtual string GetClientTypeName() { - if (IsFolder && SourceType == SourceType.Channel && this is not Channel) + if (IsFolder && SourceType == SourceType.Channel && this is not Channel && this is not Season && this is not Series) { return "ChannelFolderItem"; } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 1293528fba..408161b03d 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json.Serialization; +using System.Threading; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; @@ -152,6 +153,21 @@ namespace MediaBrowser.Controller.Entities.TV protected override QueryResult GetItemsInternal(InternalItemsQuery query) { + if (SourceType == SourceType.Channel) + { + try + { + query.Parent = this; + query.ChannelIds = new[] { ChannelId }; + return ChannelManager.GetChannelItemsInternal(query, new Progress(), CancellationToken.None).GetAwaiter().GetResult(); + } + catch + { + // Already logged at lower levels + return new QueryResult(); + } + } + if (query.User is null) { return base.GetItemsInternal(query); diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 5dad158519..8d55576224 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -226,6 +226,21 @@ namespace MediaBrowser.Controller.Entities.TV { var user = query.User; + if (SourceType == SourceType.Channel) + { + try + { + query.Parent = this; + query.ChannelIds = [ChannelId]; + return ChannelManager.GetChannelItemsInternal(query, new Progress(), CancellationToken.None).GetAwaiter().GetResult(); + } + catch + { + // Already logged at lower levels + return new QueryResult(); + } + } + if (query.Recursive) { var seriesKey = GetUniqueSeriesKey(this); @@ -372,7 +387,25 @@ namespace MediaBrowser.Controller.Entities.TV query.IsMissing = false; } - var allItems = LibraryManager.GetItemList(query); + IReadOnlyList allItems; + if (SourceType == SourceType.Channel) + { + try + { + query.Parent = parentSeason; + query.ChannelIds = [ChannelId]; + allItems = [.. ChannelManager.GetChannelItemsInternal(query, new Progress(), CancellationToken.None).GetAwaiter().GetResult().Items]; + } + catch + { + // Already logged at lower levels + return []; + } + } + else + { + allItems = LibraryManager.GetItemList(query); + } return GetSeasonEpisodes(parentSeason, user, allItems, options, shouldIncludeMissingEpisodes); } From e9331fe9d73469bb04ae549ceaa9ea6f1ed7aa6a Mon Sep 17 00:00:00 2001 From: gnattu Date: Fri, 28 Mar 2025 08:07:54 +0800 Subject: [PATCH 133/508] Improve SkiaEncoder's font handling (#13231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve SkiaEncoder's font handling Our previous approach didn’t work with some complex library names, even when the required fonts were present, because the font handling logic was too simplistic. Modern Unicode and the fonts have become quite complex, making it challenging to implement it correctly. This improved implementation still isn’t the most correct way, but it’s better than it used to be. It now falls back to multiple fonts to find the best one and also handles extended grapheme clusters that were incorrectly processed before. * Fix space * Remove redundant comment * Make _typefaces an array * Make Measure and Draw text function name clear * Fix rename --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 40 ++++++ .../StripCollageBuilder.cs | 135 ++++++++++++++++-- 2 files changed, 161 insertions(+), 14 deletions(-) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 2dac5598f0..99f7fa7f96 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using BlurHashSharp.SkiaSharp; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; @@ -24,6 +25,7 @@ public class SkiaEncoder : IImageEncoder private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; private static readonly SKImageFilter _imageFilter; + private static readonly SKTypeface[] _typefaces; #pragma warning disable CA1810 static SkiaEncoder() @@ -46,6 +48,21 @@ public class SkiaEncoder : IImageEncoder kernelOffset, SKShaderTileMode.Clamp, true); + + // Initialize the list of typefaces + // We have to statically build a list of typefaces because MatchCharacter only accepts a single character or code point + // But in reality a human-readable character (grapheme cluster) could be multiple code points. For example, 🚵🏻‍♀️ is a single emoji but 5 code points (U+1F6B5 + U+1F3FB + U+200D + U+2640 + U+FE0F) + _typefaces = + [ + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '鸡'), // CJK Simplified Chinese + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '雞'), // CJK Traditional Chinese + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ノ'), // CJK Japanese + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, '각'), // CJK Korean + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 128169), // Emojis, 128169 is the 💩emoji + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ז'), // Hebrew + SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ي'), // Arabic + SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) // Default font + ]; } /// @@ -97,6 +114,11 @@ public class SkiaEncoder : IImageEncoder public IReadOnlyCollection SupportedOutputFormats => new HashSet { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png, ImageFormat.Svg }; + /// + /// Gets the default typeface to use. + /// + public static SKTypeface DefaultTypeFace => _typefaces.Last(); + /// /// Check if the native lib is available. /// @@ -705,4 +727,22 @@ public class SkiaEncoder : IImageEncoder _logger.LogError(ex, "Error drawing indicator overlay"); } } + + /// + /// Return the typeface that contains the glyph for the given character. + /// + /// The text character. + /// The typeface contains the character. + public static SKTypeface? GetFontForCharacter(string c) + { + foreach (var typeface in _typefaces) + { + if (typeface.ContainsGlyphs(c)) + { + return typeface; + } + } + + return null; + } } diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 4aff26c16b..b0c9c0b3cc 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Text.RegularExpressions; using SkiaSharp; @@ -23,9 +24,6 @@ public partial class StripCollageBuilder _skiaEncoder = skiaEncoder; } - [GeneratedRegex(@"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]")] - private static partial Regex NonCjkPatternRegex(); - [GeneratedRegex(@"\p{IsArabic}|\p{IsArmenian}|\p{IsHebrew}|\p{IsSyriac}|\p{IsThaana}")] private static partial Regex IsRtlTextRegex(); @@ -123,14 +121,7 @@ public partial class StripCollageBuilder }; canvas.DrawRect(0, 0, width, height, paintColor); - var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); - - // use the system fallback to find a typeface for the given CJK character - var filteredName = NonCjkPatternRegex().Replace(libraryName ?? string.Empty, string.Empty); - if (!string.IsNullOrEmpty(filteredName)) - { - typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]); - } + var typeFace = SkiaEncoder.DefaultTypeFace; // draw library name using var textPaint = new SKPaint @@ -138,7 +129,7 @@ public partial class StripCollageBuilder Color = SKColors.White, Style = SKPaintStyle.Fill, TextSize = 112, - TextAlign = SKTextAlign.Center, + TextAlign = SKTextAlign.Left, Typeface = typeFace, IsAntialias = true }; @@ -155,13 +146,23 @@ public partial class StripCollageBuilder return bitmap; } + var realWidth = DrawText(null, 0, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint); + if (realWidth > width * 0.95) + { + textPaint.TextSize = 0.9f * width * textPaint.TextSize / realWidth; + realWidth = DrawText(null, 0, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint); + } + + var padding = (width - realWidth) / 2; + if (IsRtlTextRegex().IsMatch(libraryName)) { - canvas.DrawShapedText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); + textPaint.TextAlign = SKTextAlign.Right; + DrawText(canvas, width - padding, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint, true); } else { - canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint); + DrawText(canvas, padding, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint); } return bitmap; @@ -200,4 +201,110 @@ public partial class StripCollageBuilder return bitmap; } + + /// + /// Draw shaped text with given SKPaint. + /// + /// If not null, draw text to this canvas, otherwise only measure the text width. + /// x position of the canvas to draw text. + /// y position of the canvas to draw text. + /// The text to draw. + /// The SKPaint to style the text. + /// The width of the text. + private static float MeasureAndDrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint) + { + var width = textPaint.MeasureText(text); + canvas?.DrawShapedText(text, x, y, textPaint); + return width; + } + + /// + /// Draw shaped text with given SKPaint, search defined type faces to render as many texts as possible. + /// + /// If not null, draw text to this canvas, otherwise only measure the text width. + /// x position of the canvas to draw text. + /// y position of the canvas to draw text. + /// The text to draw. + /// The SKPaint to style the text. + /// If true, render from right to left. + /// The width of the text. + private static float DrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint, bool isRtl = false) + { + float width = 0; + + if (textPaint.ContainsGlyphs(text)) + { + // Current font can render all characters in text + return MeasureAndDrawText(canvas, x, y, text, textPaint); + } + + // Iterate over all text elements using TextElementEnumerator + // We cannot use foreach here because a human-readable character (grapheme cluster) can be multiple code points + // We cannot render character by character because glyphs do not always have same width + // And the result will look very unnatural due to the width difference and missing natural spacing + var start = 0; + var enumerator = StringInfo.GetTextElementEnumerator(text); + while (enumerator.MoveNext()) + { + bool notAtEnd; + var textElement = enumerator.GetTextElement(); + if (textPaint.ContainsGlyphs(textElement)) + { + continue; + } + + // If we get here, we have a text element which cannot be rendered with current font + // Draw previous characters which can be rendered with current font + if (start != enumerator.ElementIndex) + { + var regularText = text.Substring(start, enumerator.ElementIndex - start); + width += MeasureAndDrawText(canvas, MoveX(x, width), y, regularText, textPaint); + start = enumerator.ElementIndex; + } + + // Search for next point where current font can render the character there + while ((notAtEnd = enumerator.MoveNext()) && !textPaint.ContainsGlyphs(enumerator.GetTextElement())) + { + // Do nothing, just move enumerator to the point where current font can render the character + } + + // Now we have a substring that should pick another font + // The enumerator may or may not be already at the end of the string + var subtext = notAtEnd + ? text.Substring(start, enumerator.ElementIndex - start) + : text[start..]; + + var fallback = SkiaEncoder.GetFontForCharacter(textElement); + + if (fallback is not null) + { + using var fallbackTextPaint = new SKPaint(); + fallbackTextPaint.Color = textPaint.Color; + fallbackTextPaint.Style = textPaint.Style; + fallbackTextPaint.TextSize = textPaint.TextSize; + fallbackTextPaint.TextAlign = textPaint.TextAlign; + fallbackTextPaint.Typeface = fallback; + fallbackTextPaint.IsAntialias = textPaint.IsAntialias; + + // Do the search recursively to select all possible fonts + width += DrawText(canvas, MoveX(x, width), y, subtext, fallbackTextPaint, isRtl); + } + else + { + // Used up all fonts and no fonts can be found, just use current font + width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint); + } + + start = notAtEnd ? enumerator.ElementIndex : text.Length; + } + + // Render the remaining text that current fonts can render + if (start < text.Length) + { + width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint); + } + + return width; + float MoveX(float currentX, float dWidth) => isRtl ? currentX - dWidth : currentX + dWidth; + } } From 88ceaa39b0347c7b7626d38a48baa64923c66eeb Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Thu, 27 Mar 2025 18:16:54 -0600 Subject: [PATCH 134/508] Implement limiting caches (#13605) * Implement basic expiring cache for LibraryManager * Add expiring cache to more places * Rider why * Make DirectoryService caches static * Use FastConcurrentLru * Reduce default cache size * Simplify DirectoryService caches * Make directory service cache size at least 128 --- Directory.Packages.props | 3 ++- .../Emby.Server.Implementations.csproj | 1 + .../Library/LibraryManager.cs | 19 ++++++++++--------- .../Library/UserDataManager.cs | 14 ++++++-------- .../MediaBrowser.Controller.csproj | 1 + .../Providers/DirectoryService.cs | 17 ++++++++--------- .../Configuration/ServerConfiguration.cs | 5 +++++ .../DirectoryServiceTests.cs | 8 ++++---- 8 files changed, 37 insertions(+), 31 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 89311142cd..f9e111b389 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,6 +9,7 @@ + @@ -87,4 +88,4 @@ - + \ No newline at end of file diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 8f89f35ac9..6722c20da6 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -22,6 +22,7 @@ + diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index eb8e310729..62f1f3d3aa 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2,7 +2,6 @@ #pragma warning disable CA5394 using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -11,6 +10,7 @@ using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using BitFaster.Caching.Lru; using Emby.Naming.Common; using Emby.Naming.TV; using Emby.Server.Implementations.Library.Resolvers; @@ -64,7 +64,6 @@ namespace Emby.Server.Implementations.Library private const string ShortcutFileExtension = ".mblink"; private readonly ILogger _logger; - private readonly ConcurrentDictionary _cache; private readonly ITaskManager _taskManager; private readonly IUserManager _userManager; private readonly IUserDataManager _userDataRepository; @@ -81,6 +80,7 @@ namespace Emby.Server.Implementations.Library private readonly IPeopleRepository _peopleRepository; private readonly ExtraResolver _extraResolver; private readonly IPathManager _pathManager; + private readonly FastConcurrentLru _cache; /// /// The _root folder sync lock. @@ -150,7 +150,9 @@ namespace Emby.Server.Implementations.Library _mediaEncoder = mediaEncoder; _itemRepository = itemRepository; _imageProcessor = imageProcessor; - _cache = new ConcurrentDictionary(); + + _cache = new FastConcurrentLru(_configurationManager.Configuration.CacheSize); + _namingOptions = namingOptions; _peopleRepository = peopleRepository; _pathManager = pathManager; @@ -158,7 +160,7 @@ namespace Emby.Server.Implementations.Library _configurationManager.ConfigurationUpdated += ConfigurationUpdated; - RecordConfigurationValues(configurationManager.Configuration); + RecordConfigurationValues(_configurationManager.Configuration); } /// @@ -306,7 +308,7 @@ namespace Emby.Server.Implementations.Library } } - _cache[item.Id] = item; + _cache.AddOrUpdate(item.Id, item); } public void DeleteItem(BaseItem item, DeleteOptions options) @@ -460,14 +462,13 @@ namespace Emby.Server.Implementations.Library item.SetParent(null); _itemRepository.DeleteItem(item.Id); + _cache.TryRemove(item.Id, out _); foreach (var child in children) { _itemRepository.DeleteItem(child.Id); _cache.TryRemove(child.Id, out _); } - _cache.TryRemove(item.Id, out _); - ReportItemRemoved(item, parent); } @@ -1255,7 +1256,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException("Guid can't be empty", nameof(id)); } - if (_cache.TryGetValue(id, out BaseItem? item)) + if (_cache.TryGet(id, out var item)) { return item; } @@ -1272,7 +1273,7 @@ namespace Emby.Server.Implementations.Library /// public T? GetItemById(Guid id) - where T : BaseItem + where T : BaseItem { var item = GetItemById(id); if (item is T typedItem) diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 8b88b904bb..be1d96bf0b 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -1,14 +1,13 @@ #pragma warning disable RS0030 // Do not use banned APIs using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; +using BitFaster.Caching.Lru; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; -using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -26,11 +25,9 @@ namespace Emby.Server.Implementations.Library /// public class UserDataManager : IUserDataManager { - private readonly ConcurrentDictionary _userData = - new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - private readonly IServerConfigurationManager _config; private readonly IDbContextFactory _repository; + private readonly FastConcurrentLru _cache; /// /// Initializes a new instance of the class. @@ -43,6 +40,7 @@ namespace Emby.Server.Implementations.Library { _config = config; _repository = repository; + _cache = new FastConcurrentLru(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase); } /// @@ -81,7 +79,7 @@ namespace Emby.Server.Implementations.Library var userId = user.InternalId; var cacheKey = GetCacheKey(userId, item.Id); - _userData.AddOrUpdate(cacheKey, userData, (_, _) => userData); + _cache.AddOrUpdate(cacheKey, userData); UserDataSaved?.Invoke(this, new UserDataSaveEventArgs { @@ -182,7 +180,7 @@ namespace Emby.Server.Implementations.Library { var cacheKey = GetCacheKey(user.InternalId, itemId); - if (_userData.TryGetValue(cacheKey, out var data)) + if (_cache.TryGet(cacheKey, out var data)) { return data; } @@ -197,7 +195,7 @@ namespace Emby.Server.Implementations.Library }; } - return _userData.GetOrAdd(cacheKey, data); + return _cache.GetOrAdd(cacheKey, _ => data); } private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List keys) diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index ba4a2a59c4..d8aaf5ba01 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -18,6 +18,7 @@ + diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 474f09dc5e..4fca944771 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -1,23 +1,22 @@ #pragma warning disable CS1591 using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using BitFaster.Caching.Lru; using MediaBrowser.Model.IO; namespace MediaBrowser.Controller.Providers { public class DirectoryService : IDirectoryService { + // These caches are primarily used for scanning so no reason to have them be large. + private static readonly FastConcurrentLru _cache = new(Environment.ProcessorCount, Math.Max(128, Environment.ProcessorCount * 10), StringComparer.Ordinal); + private static readonly FastConcurrentLru _fileCache = new(Environment.ProcessorCount, Math.Max(128, Environment.ProcessorCount * 10), StringComparer.Ordinal); + private static readonly FastConcurrentLru> _filePathCache = new(Environment.ProcessorCount, Math.Max(128, Environment.ProcessorCount * 10), StringComparer.Ordinal); + private readonly IFileSystem _fileSystem; - private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal); - - private readonly ConcurrentDictionary _fileCache = new(StringComparer.Ordinal); - - private readonly ConcurrentDictionary> _filePathCache = new(StringComparer.Ordinal); - public DirectoryService(IFileSystem fileSystem) { _fileSystem = fileSystem; @@ -74,13 +73,13 @@ namespace MediaBrowser.Controller.Providers public FileSystemMetadata? GetFileSystemEntry(string path) { - if (!_fileCache.TryGetValue(path, out var result)) + if (!_fileCache.TryGet(path, out var result)) { var file = _fileSystem.GetFileSystemInfo(path); if (file?.Exists ?? false) { result = file; - _fileCache.TryAdd(path, result); + _fileCache.AddOrUpdate(path, result); } } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 693bf90e71..f4e6c8e2c5 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -177,6 +177,11 @@ public class ServerConfiguration : BaseApplicationConfiguration /// The library update duration. public int LibraryUpdateDuration { get; set; } = 30; + /// + /// Gets or sets the maximum amount of items to cache. + /// + public int CacheSize { get; set; } = Environment.ProcessorCount * 100; + /// /// Gets or sets the image saving convention. /// diff --git a/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs b/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs index 07b53bf74c..9e7a8c8440 100644 --- a/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs +++ b/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs @@ -181,8 +181,8 @@ namespace Jellyfin.Controller.Tests fileSystemMock.Setup(f => f.GetFileSystemInfo(It.Is(x => x == path))).Returns(newFileSystemMetadata); var secondResult = directoryService.GetFile(path); - Assert.Equal(cachedFileSystemMetadata, result); - Assert.Equal(cachedFileSystemMetadata, secondResult); + Assert.Equivalent(cachedFileSystemMetadata, result); + Assert.Equivalent(cachedFileSystemMetadata, secondResult); } [Fact] @@ -209,7 +209,7 @@ namespace Jellyfin.Controller.Tests fileSystemMock.Setup(f => f.GetFilePaths(It.Is(x => x == path), false)).Returns(cachedPaths); var directoryService = new DirectoryService(fileSystemMock.Object); - var result = directoryService.GetFilePaths(path); + var result = directoryService.GetFilePaths(path, true); fileSystemMock.Setup(f => f.GetFilePaths(It.Is(x => x == path), false)).Returns(newPaths); var secondResult = directoryService.GetFilePaths(path); @@ -241,7 +241,7 @@ namespace Jellyfin.Controller.Tests fileSystemMock.Setup(f => f.GetFilePaths(It.Is(x => x == path), false)).Returns(cachedPaths); var directoryService = new DirectoryService(fileSystemMock.Object); - var result = directoryService.GetFilePaths(path); + var result = directoryService.GetFilePaths(path, true); fileSystemMock.Setup(f => f.GetFilePaths(It.Is(x => x == path), false)).Returns(newPaths); var secondResult = directoryService.GetFilePaths(path, true); From f02190c394ad300c00dec4cc4f3b9d42ac6aaca2 Mon Sep 17 00:00:00 2001 From: Michael McElroy <44167199+mcmcelro@users.noreply.github.com> Date: Thu, 27 Mar 2025 20:18:19 -0400 Subject: [PATCH 135/508] Fix for Issue #12142: Fix ExtraRuleResolver filtering out top level folders (#12170) * Fix ExtraRuleResolver to stop filtering out libraries where the name of the base folder matches an 'videos extras' rule with an ExtraRuleType of DirectoryName Currently the ExtraRuleResolver code doesn't know anything about the root folder of the current library. As a result, when we're attempting to add items in a library where the root folder has a name with a match in Emby.Naming.Common.NamingOptions.VideoExtraRules, the entire library is being ignored as a Video Extras folder. Need to pass in the root folder of the current library to compare to the path of the current item being evaluated, and if we match the current item's folder to the root folder, then we ignore the ExtraRules with a type of DirectoryName and we continue to scan deeper in the library. Filters still apply to subfolders within the library itself. * Update CONTRIBUTORS.md * Update Emby.Naming/Video/ExtraRuleResolver.cs * Update ExtraTests.cs Add tests for this fix. Also add missing tests in TestKodiExtras, TestExpandedExtras, and TestSample, and expanded TestDirectories into TestDirectoriesAudioExtras and TestDirectoriesVideoExtras. There were no checks for the theme-music folder name previously. * Update ExtraTests.cs Removed unnecessary "using System" * In MediaBrowser.Model, upgrade System.Text.Json from 8.0.3 (vulnerable - high risk) to 8.0.4 * Update ExtraTests.cs Remove empty lines in usings * Revert "In MediaBrowser.Model, upgrade System.Text.Json from 8.0.3 (vulnerable - high risk) to 8.0.4" --- CONTRIBUTORS.md | 1 + Emby.Naming/Video/ExtraRuleResolver.cs | 7 ++- Emby.Naming/Video/VideoListResolver.cs | 5 +- Emby.Naming/Video/VideoResolver.cs | 15 +++-- .../Library/LibraryManager.cs | 2 +- .../Library/Resolvers/ExtraResolver.cs | 4 +- .../Library/Resolvers/Movies/MovieResolver.cs | 4 +- .../Jellyfin.Naming.Tests/Video/ExtraTests.cs | 62 ++++++++++++++++++- 8 files changed, 84 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ae1a2fd71e..0dcce1ea18 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -269,3 +269,4 @@ - [Robert Lützner](https://github.com/rluetzner) - [Nathan McCrina](https://github.com/nfmccrina) - [Martin Reuter](https://github.com/reuterma24) + - [Michael McElroy](https://github.com/mcmcelro) diff --git a/Emby.Naming/Video/ExtraRuleResolver.cs b/Emby.Naming/Video/ExtraRuleResolver.cs index 3219472eff..5289065898 100644 --- a/Emby.Naming/Video/ExtraRuleResolver.cs +++ b/Emby.Naming/Video/ExtraRuleResolver.cs @@ -18,8 +18,9 @@ namespace Emby.Naming.Video /// /// Path to file. /// The naming options. + /// Top-level folder for the containing library. /// Returns object. - public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions) + public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions, string? libraryRoot = "") { var result = new ExtraResult(); @@ -69,7 +70,9 @@ namespace Emby.Naming.Video else if (rule.RuleType == ExtraRuleType.DirectoryName) { var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan)); - if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)) + string fullDirectory = Path.GetDirectoryName(pathSpan).ToString(); + if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase) + && !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase)) { result.ExtraType = rule.ExtraType; result.Rule = rule; diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 12bc22a6ac..a3134f3f68 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -27,8 +27,9 @@ namespace Emby.Naming.Video /// The naming options. /// Indication we should consider multi-versions of content. /// Whether to parse the name or use the filename. + /// Top-level folder for the containing library. /// Returns enumerable of which groups files together when related. - public static IReadOnlyList Resolve(IReadOnlyList videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true) + public static IReadOnlyList Resolve(IReadOnlyList videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "") { // Filter out all extras, otherwise they could cause stacks to not be resolved // See the unit test TestStackedWithTrailer @@ -65,7 +66,7 @@ namespace Emby.Naming.Video { var info = new VideoInfo(stack.Name) { - Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName)) + Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName, libraryRoot)) .OfType() .ToList() }; diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs index db5bfdbf94..afbf6f8fae 100644 --- a/Emby.Naming/Video/VideoResolver.cs +++ b/Emby.Naming/Video/VideoResolver.cs @@ -17,10 +17,11 @@ namespace Emby.Naming.Video /// The path. /// The naming options. /// Whether to parse the name or use the filename. + /// Top-level folder for the containing library. /// VideoFileInfo. - public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true) + public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "") { - return Resolve(path, true, namingOptions, parseName); + return Resolve(path, true, namingOptions, parseName, libraryRoot); } /// @@ -28,10 +29,11 @@ namespace Emby.Naming.Video /// /// The path. /// The naming options. + /// Top-level folder for the containing library. /// VideoFileInfo. - public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions) + public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions, string? libraryRoot = "") { - return Resolve(path, false, namingOptions); + return Resolve(path, false, namingOptions, libraryRoot: libraryRoot); } /// @@ -41,9 +43,10 @@ namespace Emby.Naming.Video /// if set to true [is folder]. /// The naming options. /// Whether or not the name should be parsed for info. + /// Top-level folder for the containing library. /// VideoFileInfo. /// path is null. - public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true) + public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "") { if (string.IsNullOrEmpty(path)) { @@ -75,7 +78,7 @@ namespace Emby.Naming.Video var format3DResult = Format3DParser.Parse(path, namingOptions); - var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions); + var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions, libraryRoot); var name = Path.GetFileNameWithoutExtension(path); diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 62f1f3d3aa..c8026960df 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2709,7 +2709,7 @@ namespace Emby.Server.Implementations.Library public IEnumerable FindExtras(BaseItem owner, IReadOnlyList fileSystemChildren, IDirectoryService directoryService) { - var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions); + var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath); if (ownerVideoInfo is null) { yield break; diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs index b4791b9456..b9f9f29723 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs @@ -54,9 +54,9 @@ namespace Emby.Server.Implementations.Library.Resolvers _ => _videoResolvers }; - public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType) + public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "") { - var extraResult = GetExtraInfo(path, _namingOptions); + var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot); if (extraResult.ExtraType is null) { extraType = null; diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 4debe722b9..f1aeb1340a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -270,11 +270,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } var videoInfos = files - .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName)) + .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath)) .Where(f => f is not null) .ToList(); - var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName); + var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath); var result = new MultiItemResolverResult { diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs index 2c33ab4929..51eb99f496 100644 --- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs @@ -2,6 +2,7 @@ using Emby.Naming.Common; using Emby.Naming.Video; using MediaBrowser.Model.Entities; using Xunit; + using MediaType = Emby.Naming.Common.MediaType; namespace Jellyfin.Naming.Tests.Video @@ -20,6 +21,9 @@ namespace Jellyfin.Naming.Tests.Video { Test("trailer.mp4", ExtraType.Trailer); Test("300-trailer.mp4", ExtraType.Trailer); + Test("300.trailer.mp4", ExtraType.Trailer); + Test("300_trailer.mp4", ExtraType.Trailer); + Test("300 trailer.mp4", ExtraType.Trailer); Test("theme.mp3", ExtraType.ThemeSong); } @@ -43,6 +47,19 @@ namespace Jellyfin.Naming.Tests.Video Test("300-deletedscene.mp4", ExtraType.DeletedScene); Test("300-interview.mp4", ExtraType.Interview); Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes); + Test("300-featurette.mp4", ExtraType.Featurette); + Test("300-short.mp4", ExtraType.Short); + Test("300-extra.mp4", ExtraType.Unknown); + Test("300-other.mp4", ExtraType.Unknown); + } + + [Theory] + [InlineData(ExtraType.ThemeSong, "theme-music")] + public void TestDirectoriesAudioExtras(ExtraType type, string dirName) + { + Test(dirName + "/300.mp3", type); + Test("300/" + dirName + "/something.mp3", type); + Test("/data/something/Movies/300/" + dirName + "/whoknows.mp3", type); } [Theory] @@ -52,11 +69,14 @@ namespace Jellyfin.Naming.Tests.Video [InlineData(ExtraType.Scene, "scenes")] [InlineData(ExtraType.Sample, "samples")] [InlineData(ExtraType.Short, "shorts")] + [InlineData(ExtraType.Trailer, "trailers")] [InlineData(ExtraType.Featurette, "featurettes")] [InlineData(ExtraType.Clip, "clips")] [InlineData(ExtraType.ThemeVideo, "backdrops")] + [InlineData(ExtraType.Unknown, "extra")] [InlineData(ExtraType.Unknown, "extras")] - public void TestDirectories(ExtraType type, string dirName) + [InlineData(ExtraType.Unknown, "other")] + public void TestDirectoriesVideoExtras(ExtraType type, string dirName) { Test(dirName + "/300.mp4", type); Test("300/" + dirName + "/something.mkv", type); @@ -75,10 +95,44 @@ namespace Jellyfin.Naming.Tests.Video Test("/data/something/Movies/" + dirName + "/" + dirName + ".mp4", null); } + [Theory] + [InlineData(ExtraType.ThemeSong, "theme-music")] + public void TestTopLevelDirectoriesWithAudioExtraNames(ExtraType typicalType, string dirName) + { + string libraryRoot = "/data/something/" + dirName; + TestWithLibraryRoot(libraryRoot + "/300.mp3", libraryRoot, null); + TestWithLibraryRoot(libraryRoot + "/300/" + dirName + "/something.mp3", libraryRoot, typicalType); + } + + [Theory] + [InlineData(ExtraType.Trailer, "trailers")] + [InlineData(ExtraType.ThemeVideo, "backdrops")] + [InlineData(ExtraType.BehindTheScenes, "behind the scenes")] + [InlineData(ExtraType.DeletedScene, "deleted scenes")] + [InlineData(ExtraType.Interview, "interviews")] + [InlineData(ExtraType.Scene, "scenes")] + [InlineData(ExtraType.Sample, "samples")] + [InlineData(ExtraType.Short, "shorts")] + [InlineData(ExtraType.Featurette, "featurettes")] + [InlineData(ExtraType.Unknown, "extras")] + [InlineData(ExtraType.Unknown, "extra")] + [InlineData(ExtraType.Unknown, "other")] + [InlineData(ExtraType.Clip, "clips")] + public void TestTopLevelDirectoriesWithVideoExtraNames(ExtraType typicalType, string dirName) + { + string libraryRoot = "/data/something/" + dirName; + TestWithLibraryRoot(libraryRoot + "/300.mp4", libraryRoot, null); + TestWithLibraryRoot(libraryRoot + "/300/" + dirName + "/something.mkv", libraryRoot, typicalType); + } + [Fact] public void TestSample() { + Test("sample.mp4", ExtraType.Sample); Test("300-sample.mp4", ExtraType.Sample); + Test("300.sample.mp4", ExtraType.Sample); + Test("300_sample.mp4", ExtraType.Sample); + Test("300 sample.mp4", ExtraType.Sample); } private void Test(string input, ExtraType? expectedType) @@ -88,6 +142,12 @@ namespace Jellyfin.Naming.Tests.Video Assert.Equal(expectedType, extraType); } + private void TestWithLibraryRoot(string input, string libraryRoot, ExtraType? expectedType) + { + var extraType = ExtraRuleResolver.GetExtraInfo(input, _videoOptions, libraryRoot).ExtraType; + Assert.Equal(expectedType, extraType); + } + [Fact] public void TestExtraInfo_InvalidRuleType() { From 6c46b06c755c1539a5e743d72318292fea72fe32 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Thu, 27 Mar 2025 19:46:45 -0600 Subject: [PATCH 136/508] Fix merged namespace error --- Jellyfin.Server.Implementations/Item/OrderMapper.cs | 2 +- .../Item/OrderMapperTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs index df2fd08b17..03249b9274 100644 --- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs +++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs @@ -1,8 +1,8 @@ using System; using System.Linq; using System.Linq.Expressions; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Controller.Entities; using Microsoft.EntityFrameworkCore; diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs index a6d6b03400..caf2b06b73 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Item/OrderMapperTests.cs @@ -1,6 +1,6 @@ using System; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations.Entities; using Jellyfin.Server.Implementations.Item; using MediaBrowser.Controller.Entities; using Xunit; From 15465afd8e7f8a72dd79654f458f8c298e776e02 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Thu, 27 Mar 2025 21:10:27 -0600 Subject: [PATCH 137/508] Revert changes to DirectoryService --- .../Providers/DirectoryService.cs | 16 +++++++++------- .../DirectoryServiceTests.cs | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 4fca944771..a1edfa3c96 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -1,19 +1,21 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using BitFaster.Caching.Lru; using MediaBrowser.Model.IO; namespace MediaBrowser.Controller.Providers { public class DirectoryService : IDirectoryService { - // These caches are primarily used for scanning so no reason to have them be large. - private static readonly FastConcurrentLru _cache = new(Environment.ProcessorCount, Math.Max(128, Environment.ProcessorCount * 10), StringComparer.Ordinal); - private static readonly FastConcurrentLru _fileCache = new(Environment.ProcessorCount, Math.Max(128, Environment.ProcessorCount * 10), StringComparer.Ordinal); - private static readonly FastConcurrentLru> _filePathCache = new(Environment.ProcessorCount, Math.Max(128, Environment.ProcessorCount * 10), StringComparer.Ordinal); + // TODO make static and switch to FastConcurrentLru. + private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal); + + private readonly ConcurrentDictionary _fileCache = new(StringComparer.Ordinal); + + private readonly ConcurrentDictionary> _filePathCache = new(StringComparer.Ordinal); private readonly IFileSystem _fileSystem; @@ -73,13 +75,13 @@ namespace MediaBrowser.Controller.Providers public FileSystemMetadata? GetFileSystemEntry(string path) { - if (!_fileCache.TryGet(path, out var result)) + if (!_fileCache.TryGetValue(path, out var result)) { var file = _fileSystem.GetFileSystemInfo(path); if (file?.Exists ?? false) { result = file; - _fileCache.AddOrUpdate(path, result); + _fileCache.TryAdd(path, result); } } diff --git a/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs b/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs index 9e7a8c8440..1f59908a86 100644 --- a/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs +++ b/tests/Jellyfin.Controller.Tests/DirectoryServiceTests.cs @@ -209,7 +209,7 @@ namespace Jellyfin.Controller.Tests fileSystemMock.Setup(f => f.GetFilePaths(It.Is(x => x == path), false)).Returns(cachedPaths); var directoryService = new DirectoryService(fileSystemMock.Object); - var result = directoryService.GetFilePaths(path, true); + var result = directoryService.GetFilePaths(path); fileSystemMock.Setup(f => f.GetFilePaths(It.Is(x => x == path), false)).Returns(newPaths); var secondResult = directoryService.GetFilePaths(path); @@ -241,7 +241,7 @@ namespace Jellyfin.Controller.Tests fileSystemMock.Setup(f => f.GetFilePaths(It.Is(x => x == path), false)).Returns(cachedPaths); var directoryService = new DirectoryService(fileSystemMock.Object); - var result = directoryService.GetFilePaths(path, true); + var result = directoryService.GetFilePaths(path); fileSystemMock.Setup(f => f.GetFilePaths(It.Is(x => x == path), false)).Returns(newPaths); var secondResult = directoryService.GetFilePaths(path, true); From a2ef0e4abec6ae78bd43089c46064e1e0943d8a5 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 28 Mar 2025 08:08:18 +0100 Subject: [PATCH 138/508] Fix trusting all sources for forward headers if none are configured --- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index c6c3f21fe1..b04e55baa6 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -119,15 +119,15 @@ namespace Jellyfin.Server.Extensions // https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs // Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues. - options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; - if (config.KnownProxies.Length == 0) { + options.ForwardedHeaders = ForwardedHeaders.None; options.KnownNetworks.Clear(); options.KnownProxies.Clear(); } else { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; AddProxyAddresses(config, config.KnownProxies, options); } From 6d7950bddc3733b02e8ba8968c8264cf115a52dc Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 27 Mar 2025 16:10:20 +0100 Subject: [PATCH 139/508] Fix container parameter validation --- Jellyfin.Api/Controllers/AudioController.cs | 2 +- Jellyfin.Api/Controllers/DynamicHlsController.cs | 4 ++-- Jellyfin.Api/Controllers/LiveTvController.cs | 4 +++- Jellyfin.Api/Controllers/VideosController.cs | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 272b4034ed..33f21db714 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -259,7 +259,7 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task GetAudioStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ValidationRegex)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index ca8ab0ef75..90dcbd59e2 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1106,7 +1106,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ValidationRegex)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1291,7 +1291,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ValidationRegex)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 5461d12fa2..172686cadb 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -1190,7 +1190,9 @@ public class LiveTvController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesVideoFile] - public ActionResult GetLiveStreamFile([FromRoute, Required] string streamId, [FromRoute, Required] string container) + public ActionResult GetLiveStreamFile( + [FromRoute, Required] string streamId, + [FromRoute, Required] [RegularExpression(EncodingHelper.ValidationRegex)] string container) { var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId); if (liveStreamInfo is null) diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 6f18c1603b..685d502e42 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -556,7 +556,7 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public Task GetVideoStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ValidationRegex)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, From 1fcc79316dc236e0b070c3d695477d6e87f4c229 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 27 Mar 2025 16:11:24 +0100 Subject: [PATCH 140/508] Rename ValidationRegex to ContainerValidationRegex --- Jellyfin.Api/Controllers/AudioController.cs | 20 +++--- .../Controllers/DynamicHlsController.cs | 62 +++++++++---------- Jellyfin.Api/Controllers/LiveTvController.cs | 2 +- .../Controllers/UniversalAudioController.cs | 4 +- Jellyfin.Api/Controllers/VideosController.cs | 20 +++--- .../MediaEncoding/EncodingHelper.cs | 10 +-- 6 files changed, 59 insertions(+), 59 deletions(-) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 33f21db714..9e3065824f 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -92,18 +92,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task GetAudioStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -133,8 +133,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -259,18 +259,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task GetAudioStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -300,8 +300,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 90dcbd59e2..f59ce3834b 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -166,18 +166,18 @@ public class DynamicHlsController : BaseJellyfinApiController [ProducesPlaylistFile] public async Task GetLiveHlsStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -207,8 +207,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -415,12 +415,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -452,8 +452,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -591,12 +591,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -627,8 +627,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -761,12 +761,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -798,8 +798,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -933,12 +933,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -969,8 +969,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1106,7 +1106,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1114,12 +1114,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1151,8 +1151,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1291,7 +1291,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1299,12 +1299,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1335,8 +1335,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 172686cadb..10f1789ad8 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -1192,7 +1192,7 @@ public class LiveTvController : BaseJellyfinApiController [ProducesVideoFile] public ActionResult GetLiveStreamFile( [FromRoute, Required] string streamId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ValidationRegex)] string container) + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container) { var liveStreamInfo = _mediaSourceManager.GetLiveStreamInfoByUniqueId(streamId); if (liveStreamInfo is null) diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index a5b5fde626..fd63347030 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -102,13 +102,13 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, [FromQuery] Guid? userId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] int? maxAudioChannels, [FromQuery] int? transcodingAudioChannels, [FromQuery] int? maxStreamingBitrate, [FromQuery] int? audioBitRate, [FromQuery] long? startTimeTicks, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? transcodingContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer, [FromQuery] MediaStreamProtocol? transcodingProtocol, [FromQuery] int? maxAudioSampleRate, [FromQuery] int? maxAudioBitDepth, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 685d502e42..714d4ba72a 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -315,18 +315,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public async Task GetVideoStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? container, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -358,8 +358,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -556,18 +556,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public Task GetVideoStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ValidationRegex)] string container, + [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? segmentContainer, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? audioCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -599,8 +599,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] string? subtitleCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? videoCodec, + [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index cf76f336c8..ed80de6355 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -37,7 +37,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// periods, underscores, commas, and vertical bars, with a length between 0 and 40 characters. /// This should matches all common valid codecs. /// - public const string ValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; + public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; private const string _defaultMjpegEncoder = "mjpeg"; @@ -77,7 +77,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1); private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0); - private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled); + private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled); private static readonly string[] _videoProfilesH264 = [ @@ -452,7 +452,7 @@ namespace MediaBrowser.Controller.MediaEncoding return GetMjpegEncoder(state, encodingOptions); } - if (_validationRegex.IsMatch(codec)) + if (_containerValidationRegex.IsMatch(codec)) { return codec.ToLowerInvariant(); } @@ -493,7 +493,7 @@ namespace MediaBrowser.Controller.MediaEncoding public static string GetInputFormat(string container) { - if (string.IsNullOrEmpty(container) || !_validationRegex.IsMatch(container)) + if (string.IsNullOrEmpty(container) || !_containerValidationRegex.IsMatch(container)) { return null; } @@ -711,7 +711,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var codec = state.OutputAudioCodec; - if (!_validationRegex.IsMatch(codec)) + if (!_containerValidationRegex.IsMatch(codec)) { codec = "aac"; } From 3df7d7a809d14e3ab89001d4c98203e20cad9082 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 27 Mar 2025 16:13:07 +0100 Subject: [PATCH 141/508] Add validation for level input --- Jellyfin.Api/Controllers/AudioController.cs | 4 ++-- Jellyfin.Api/Controllers/DynamicHlsController.cs | 14 +++++++------- Jellyfin.Api/Controllers/VideosController.cs | 4 ++-- .../MediaEncoding/EncodingHelper.cs | 6 ++++++ 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 9e3065824f..e334e12640 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -114,7 +114,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -281,7 +281,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index f59ce3834b..a351c1be15 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -188,7 +188,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -431,7 +431,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -608,7 +608,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -777,7 +777,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -950,7 +950,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1130,7 +1130,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1316,7 +1316,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 714d4ba72a..97f3239bbc 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -337,7 +337,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -578,7 +578,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] string? level, + [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegex)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index ed80de6355..17fb42fccc 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -39,6 +39,12 @@ namespace MediaBrowser.Controller.MediaEncoding /// public const string ContainerValidationRegex = @"^[a-zA-Z0-9\-\._,|]{0,40}$"; + /// + /// The level validation regex. + /// This regular expression matches strings representing a double. + /// + public const string LevelValidationRegex = @"-?\d+(?:\.\d+)?"; + private const string _defaultMjpegEncoder = "mjpeg"; private const string QsvAlias = "qs"; From cb931e00627559e4e9d14d2cc7d4ec8e00eb7061 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:51:22 +0300 Subject: [PATCH 142/508] Add profile condition to limit the number of streams (#13583) --- MediaBrowser.Model/Dlna/ConditionProcessor.cs | 4 + .../Dlna/ProfileConditionValue.cs | 3 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 10 +- MediaBrowser.Model/Session/TranscodeReason.cs | 1 + .../Dlna/StreamBuilderTests.cs | 6 + .../DeviceProfile-Tizen3-stereo.json | 15 + .../DeviceProfile-Tizen4-4K-5.1.json | 15 + .../MediaSourceInfo-numstreams-32.json | 565 +++++++++++++++++ .../MediaSourceInfo-numstreams-33.json | 582 ++++++++++++++++++ 9 files changed, 1198 insertions(+), 3 deletions(-) create mode 100644 tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-32.json create mode 100644 tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-33.json diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs index 1b046f54ea..09b9663679 100644 --- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -28,6 +28,7 @@ namespace MediaBrowser.Model.Dlna /// A value indicating whether the video is anamorphic. /// A value indicating whether the video is interlaced. /// The reference frames. + /// The number of streams. /// The number of video streams. /// The number of audio streams. /// The video codec tag. @@ -48,6 +49,7 @@ namespace MediaBrowser.Model.Dlna bool? isAnamorphic, bool? isInterlaced, int? refFrames, + int numStreams, int? numVideoStreams, int? numAudioStreams, string? videoCodecTag, @@ -83,6 +85,8 @@ namespace MediaBrowser.Model.Dlna return IsConditionSatisfied(condition, width); case ProfileConditionValue.RefFrames: return IsConditionSatisfied(condition, refFrames); + case ProfileConditionValue.NumStreams: + return IsConditionSatisfied(condition, numStreams); case ProfileConditionValue.NumAudioStreams: return IsConditionSatisfied(condition, numAudioStreams); case ProfileConditionValue.NumVideoStreams: diff --git a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs index a32433e185..b66a15840b 100644 --- a/MediaBrowser.Model/Dlna/ProfileConditionValue.cs +++ b/MediaBrowser.Model/Dlna/ProfileConditionValue.cs @@ -27,6 +27,7 @@ namespace MediaBrowser.Model.Dlna IsInterlaced = 21, AudioSampleRate = 22, AudioBitDepth = 23, - VideoRangeType = 24 + VideoRangeType = 24, + NumStreams = 25 } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index e48411e7a1..b12e9c2d7e 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -338,6 +338,9 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.IsSecondaryAudio: return TranscodeReason.SecondaryAudioNotSupported; + case ProfileConditionValue.NumStreams: + return TranscodeReason.StreamCountExceedsLimit; + case ProfileConditionValue.NumAudioStreams: // TODO return 0; @@ -1019,6 +1022,7 @@ namespace MediaBrowser.Model.Dlna int? packetLength = videoStream?.PacketLength; int? refFrames = videoStream?.RefFrames; + int numStreams = item.MediaStreams.Count; int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio); int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video); @@ -1027,7 +1031,7 @@ namespace MediaBrowser.Model.Dlna var appliedVideoConditions = options.Profile.CodecProfiles .Where(i => i.Type == CodecType.Video && i.ContainsAnyCodec(playlistItem.VideoCodecs, container, useSubContainer) && - i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))) + i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc))) // Reverse codec profiles for backward compatibility - first codec profile has higher priority .Reverse(); foreach (var condition in appliedVideoConditions) @@ -1850,6 +1854,7 @@ namespace MediaBrowser.Model.Dlna case ProfileConditionValue.AudioProfile: case ProfileConditionValue.Has64BitOffsets: case ProfileConditionValue.PacketLength: + case ProfileConditionValue.NumStreams: case ProfileConditionValue.NumAudioStreams: case ProfileConditionValue.NumVideoStreams: case ProfileConditionValue.IsSecondaryAudio: @@ -2258,10 +2263,11 @@ namespace MediaBrowser.Model.Dlna int? packetLength = videoStream?.PacketLength; int? refFrames = videoStream?.RefFrames; + int numStreams = mediaSource.MediaStreams.Count; int? numAudioStreams = mediaSource.GetStreamCount(MediaStreamType.Audio); int? numVideoStreams = mediaSource.GetStreamCount(MediaStreamType.Video); - return conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)); + return conditions.Where(applyCondition => !ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numStreams, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)); } /// diff --git a/MediaBrowser.Model/Session/TranscodeReason.cs b/MediaBrowser.Model/Session/TranscodeReason.cs index 39c5ac8fa4..902bab9a6e 100644 --- a/MediaBrowser.Model/Session/TranscodeReason.cs +++ b/MediaBrowser.Model/Session/TranscodeReason.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Session SubtitleCodecNotSupported = 1 << 3, AudioIsExternal = 1 << 4, SecondaryAudioNotSupported = 1 << 5, + StreamCountExceedsLimit = 1 << 26, // Video Constraints VideoProfileNotSupported = 1 << 6, diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index bd2143f252..db9791263e 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -39,6 +39,8 @@ namespace Jellyfin.Model.Tests [InlineData("Chrome", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] [InlineData("Chrome", "mkv-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.ContainerNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] [InlineData("Chrome", "mp4-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode", "HLS.mp4")] + [InlineData("Chrome", "numstreams-32", PlayMethod.DirectPlay)] + [InlineData("Chrome", "numstreams-33", PlayMethod.DirectPlay)] // Firefox [InlineData("Firefox", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] // #6450 [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.Transcode, TranscodeReason.AudioCodecNotSupported, "DirectStream", "HLS.mp4")] // #6450 @@ -180,6 +182,8 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen3-stereo", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] + [InlineData("Tizen3-stereo", "numstreams-32", PlayMethod.DirectPlay)] + [InlineData("Tizen3-stereo", "numstreams-33", PlayMethod.Transcode, TranscodeReason.StreamCountExceedsLimit, "Remux")] // Tizen 4 4K 5.1 [InlineData("Tizen4-4K-5.1", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] @@ -191,6 +195,8 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] + [InlineData("Tizen4-4K-5.1", "numstreams-32", PlayMethod.DirectPlay)] + [InlineData("Tizen4-4K-5.1", "numstreams-33", PlayMethod.Transcode, TranscodeReason.StreamCountExceedsLimit, "Remux")] // WebOS 23 [InlineData("WebOS-23", "mkv-dvhe.08-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported, "Remux")] [InlineData("WebOS-23", "mp4-dvh1.05-eac3-15200k", PlayMethod.DirectPlay)] diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json index 2e3e6e6de4..895d13f074 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json @@ -510,6 +510,21 @@ "$type": "CodecProfile" } ], + "ContainerProfiles": [ + { + "Type": "Video", + "Conditions": [ + { + "Condition": "LessThanEqual", + "Property": "NumStreams", + "Value": "32", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "$type": "ContainerProfile" + } + ], "ResponseProfiles": [ { "Container": "m4v", diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json index 1562304718..345d387251 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json @@ -483,6 +483,21 @@ "$type": "CodecProfile" } ], + "ContainerProfiles": [ + { + "Type": "Video", + "Conditions": [ + { + "Condition": "LessThanEqual", + "Property": "NumStreams", + "Value": "32", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "$type": "ContainerProfile" + } + ], "ResponseProfiles": [ { "Container": "m4v", diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-32.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-32.json new file mode 100644 index 0000000000..6d01f81536 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-32.json @@ -0,0 +1,565 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 1, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 3, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 4, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 5, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 6, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 7, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 8, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 9, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 10, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 11, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 12, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 13, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 14, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 15, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 16, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 17, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 18, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 19, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 20, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 21, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 22, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 23, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 24, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 25, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 26, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 27, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 28, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 29, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 30, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 31, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-33.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-33.json new file mode 100644 index 0000000000..ac24500fe8 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-numstreams-33.json @@ -0,0 +1,582 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 1, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 2, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 3, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 4, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 5, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 6, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 7, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 8, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 9, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 10, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 11, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 12, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 13, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 14, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 15, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 16, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 17, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 18, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 19, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 20, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 21, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 22, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 23, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 24, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 25, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 26, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 27, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 28, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 29, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 30, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 31, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 32, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 2 +} From 9657708b384dfca474c28f673a2d79a3f3e4db9f Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Fri, 28 Mar 2025 13:51:44 +0100 Subject: [PATCH 143/508] Reduce allocations, simplifed code, faster implementation, included tests - StreamInfo.ToUrl (#9369) * Rework PR 6168 * Fix test --- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 6 +- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +- MediaBrowser.Model/Dlna/StreamInfo.cs | 400 +++++++++++------- MediaBrowser.Model/Dto/MediaSourceInfo.cs | 10 +- .../EnumerableExtensions.cs | 19 + .../Dlna/LegacyStreamInfo.cs | 224 ++++++++++ .../Dlna/StreamBuilderTests.cs | 2 +- .../Dlna/StreamInfoTests.cs | 243 +++++++++++ 8 files changed, 735 insertions(+), 171 deletions(-) create mode 100644 tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs create mode 100644 tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 7b493d3fa0..63c9c173b9 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -290,9 +290,7 @@ public class MediaInfoHelper mediaSource.SupportsDirectPlay = false; mediaSource.SupportsDirectStream = false; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-'); - mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; + mediaSource.TranscodingUrl = streamInfo.ToUrl(null, claimsPrincipal.GetToken(), "&allowVideoStreamCopy=false&allowAudioStreamCopy=false"); mediaSource.TranscodingContainer = streamInfo.Container; mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; if (streamInfo.AlwaysBurnInSubtitleWhenTranscoding) @@ -305,7 +303,7 @@ public class MediaInfoHelper if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream)) { streamInfo.PlayMethod = PlayMethod.Transcode; - mediaSource.TranscodingUrl = streamInfo.ToUrl("-", claimsPrincipal.GetToken()).TrimStart('-'); + mediaSource.TranscodingUrl = streamInfo.ToUrl(null, claimsPrincipal.GetToken(), null); if (!allowVideoStreamCopy) { diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index b12e9c2d7e..806900e9ad 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -800,7 +800,7 @@ namespace MediaBrowser.Model.Dlna options.SubtitleStreamIndex, playlistItem.PlayMethod, playlistItem.TranscodeReasons, - playlistItem.ToUrl("media:", "")); + playlistItem.ToUrl("media:", "", null)); item.Container = NormalizeMediaSourceFormatIntoSingleContainer(item.Container, options.Profile, DlnaProfileType.Video, directPlayProfile); return playlistItem; diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index e441522136..f9aab2d676 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -1,7 +1,12 @@ +#pragma warning disable CA1819 // Properties should not return arrays + using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; +using System.Text; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -871,202 +876,279 @@ public class StreamInfo /// /// The base Url. /// The access Token. + /// Optional extra query. /// A querystring representation of this object. - public string ToUrl(string baseUrl, string? accessToken) + public string ToUrl(string? baseUrl, string? accessToken, string? query) { - ArgumentException.ThrowIfNullOrEmpty(baseUrl); - - List list = []; - foreach (NameValuePair pair in BuildParams(this, accessToken)) + var sb = new StringBuilder(); + if (!string.IsNullOrEmpty(baseUrl)) { - if (string.IsNullOrEmpty(pair.Value)) - { - continue; - } - - // Try to keep the url clean by omitting defaults - if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) - && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) - && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) - && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal); - - list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); + sb.Append(baseUrl.TrimEnd('/')); } - string queryString = string.Join('&', list); - - return GetUrl(baseUrl, queryString); - } - - private string GetUrl(string baseUrl, string queryString) - { - ArgumentException.ThrowIfNullOrEmpty(baseUrl); - - string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container; - - baseUrl = baseUrl.TrimEnd('/'); - if (MediaType == DlnaProfileType.Audio) { - if (SubProtocol == MediaStreamProtocol.hls) + sb.Append("/audio/"); + } + else + { + sb.Append("/videos/"); + } + + sb.Append(ItemId); + + if (SubProtocol == MediaStreamProtocol.hls) + { + sb.Append("/master.m3u8?"); + } + else + { + sb.Append("/stream"); + + if (!string.IsNullOrEmpty(Container)) { - return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + sb.Append('.'); + sb.Append(Container); } - return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + sb.Append('?'); + } + + if (!string.IsNullOrEmpty(DeviceProfileId)) + { + sb.Append("&DeviceProfileId="); + sb.Append(DeviceProfileId); + } + + if (!string.IsNullOrEmpty(DeviceId)) + { + sb.Append("&DeviceId="); + sb.Append(DeviceId); + } + + if (!string.IsNullOrEmpty(MediaSourceId)) + { + sb.Append("&MediaSourceId="); + sb.Append(MediaSourceId); + } + + // default true so don't store. + if (IsDirectStream) + { + sb.Append("&Static=true"); + } + + if (VideoCodecs.Count != 0) + { + sb.Append("&VideoCodec="); + sb.AppendJoin(',', VideoCodecs); + } + + if (AudioCodecs.Count != 0) + { + sb.Append("&AudioCodec="); + sb.AppendJoin(',', AudioCodecs); + } + + if (AudioStreamIndex.HasValue) + { + sb.Append("&AudioStreamIndex="); + sb.Append(AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (SubtitleStreamIndex.HasValue && SubtitleDeliveryMethod != SubtitleDeliveryMethod.External && SubtitleStreamIndex != -1) + { + sb.Append("&SubtitleStreamIndex="); + sb.Append(SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture)); + sb.Append("&SubtitleMethod="); + sb.Append(SubtitleDeliveryMethod.ToString()); + } + + if (VideoBitrate.HasValue) + { + sb.Append("&VideoBitrate="); + sb.Append(VideoBitrate.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (AudioBitrate.HasValue) + { + sb.Append("&AudioBitrate="); + sb.Append(AudioBitrate.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (AudioSampleRate.HasValue) + { + sb.Append("&AudioSampleRate="); + sb.Append(AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (MaxFramerate.HasValue) + { + sb.Append("&MaxFramerate="); + sb.Append(MaxFramerate.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (MaxWidth.HasValue) + { + sb.Append("&MaxWidth="); + sb.Append(MaxWidth.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (MaxHeight.HasValue) + { + sb.Append("&MaxHeight="); + sb.Append(MaxHeight.Value.ToString(CultureInfo.InvariantCulture)); } if (SubProtocol == MediaStreamProtocol.hls) { - return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); - } + if (!string.IsNullOrEmpty(Container)) + { + sb.Append("&SegmentContainer="); + sb.Append(Container); + } - return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); - } + if (SegmentLength.HasValue) + { + sb.Append("&SegmentLength="); + sb.Append(SegmentLength.Value.ToString(CultureInfo.InvariantCulture)); + } - private static List BuildParams(StreamInfo item, string? accessToken) - { - List list = []; + if (MinSegments.HasValue) + { + sb.Append("&MinSegments="); + sb.Append(MinSegments.Value.ToString(CultureInfo.InvariantCulture)); + } - string audioCodecs = item.AudioCodecs.Count == 0 ? - string.Empty : - string.Join(',', item.AudioCodecs); - - string videoCodecs = item.VideoCodecs.Count == 0 ? - string.Empty : - string.Join(',', item.VideoCodecs); - - list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty)); - list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty)); - list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty)); - list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - list.Add(new NameValuePair("VideoCodec", videoCodecs)); - list.Add(new NameValuePair("AudioCodec", audioCodecs)); - list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && (item.AlwaysBurnInSubtitleWhenTranscoding || item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - - list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - - long startPositionTicks = item.StartPositionTicks; - - if (item.SubProtocol == MediaStreamProtocol.hls) - { - list.Add(new NameValuePair("StartTimeTicks", string.Empty)); + sb.Append("&BreakOnNonKeyFrames="); + sb.Append(BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)); } else { - list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture))); + if (StartPositionTicks != 0) + { + sb.Append("&StartTimeTicks="); + sb.Append(StartPositionTicks.ToString(CultureInfo.InvariantCulture)); + } } - list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); - list.Add(new NameValuePair("ApiKey", accessToken ?? string.Empty)); - - string? liveStreamId = item.MediaSource?.LiveStreamId; - list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); - - list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); - - if (!item.IsDirectStream) + if (!string.IsNullOrEmpty(PlaySessionId)) { - if (item.RequireNonAnamorphic) - { - list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - - if (item.EnableSubtitlesInManifest) - { - list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - if (item.EnableMpegtsM2TsMode) - { - list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - if (item.EstimateContentLength) - { - list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto) - { - list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant())); - } - - if (item.CopyTimestamps) - { - list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } - - list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - - list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + sb.Append("&PlaySessionId="); + sb.Append(PlaySessionId); } - list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty)); - - string subtitleCodecs = item.SubtitleCodecs.Count == 0 ? - string.Empty : - string.Join(",", item.SubtitleCodecs); - - list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); - - if (item.SubProtocol == MediaStreamProtocol.hls) + if (!string.IsNullOrEmpty(accessToken)) { - list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); - - if (item.SegmentLength.HasValue) - { - list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture))); - } - - if (item.MinSegments.HasValue) - { - list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); - } - - list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture))); + sb.Append("&ApiKey="); + sb.Append(accessToken); } - foreach (var pair in item.StreamOptions) + var liveStreamId = MediaSource?.LiveStreamId; + if (!string.IsNullOrEmpty(liveStreamId)) { - if (string.IsNullOrEmpty(pair.Value)) + sb.Append("&LiveStreamId="); + sb.Append(liveStreamId); + } + + if (!IsDirectStream) + { + if (RequireNonAnamorphic) { - continue; + sb.Append("&RequireNonAnamorphic="); + sb.Append(RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture)); } - // strip spaces to avoid having to encode h264 profile names - list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal))); + if (TranscodingMaxAudioChannels.HasValue) + { + sb.Append("&TranscodingMaxAudioChannels="); + sb.Append(TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (EnableSubtitlesInManifest) + { + sb.Append("&EnableSubtitlesInManifest="); + sb.Append(EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture)); + } + + if (EnableMpegtsM2TsMode) + { + sb.Append("&EnableMpegtsM2TsMode="); + sb.Append(EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture)); + } + + if (EstimateContentLength) + { + sb.Append("&EstimateContentLength="); + sb.Append(EstimateContentLength.ToString(CultureInfo.InvariantCulture)); + } + + if (TranscodeSeekInfo != TranscodeSeekInfo.Auto) + { + sb.Append("&TranscodeSeekInfo="); + sb.Append(TranscodeSeekInfo.ToString()); + } + + if (CopyTimestamps) + { + sb.Append("&CopyTimestamps="); + sb.Append(CopyTimestamps.ToString(CultureInfo.InvariantCulture)); + } + + if (RequireAvc) + { + sb.Append("&RequireAvc="); + sb.Append(RequireAvc.ToString(CultureInfo.InvariantCulture)); + } + + if (EnableAudioVbrEncoding) + { + sb.Append("EnableAudioVbrEncoding="); + sb.Append(EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + } } - if (!item.IsDirectStream) + var etag = MediaSource?.ETag; + if (!string.IsNullOrEmpty(etag)) { - list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString())); + sb.Append("&Tag="); + sb.Append(etag); } - return list; + if (SubtitleStreamIndex.HasValue && SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) + { + sb.Append("&SubtitleMethod="); + sb.AppendJoin(',', SubtitleDeliveryMethod); + } + + if (SubtitleStreamIndex.HasValue && SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed && SubtitleCodecs.Count != 0) + { + sb.Append("&SubtitleCodec="); + sb.AppendJoin(',', SubtitleCodecs); + } + + foreach (var pair in StreamOptions) + { + // Strip spaces to avoid having to encode h264 profile names + sb.Append('&'); + sb.Append(pair.Key); + sb.Append('='); + sb.Append(pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal)); + } + + var transcodeReasonsValues = TranscodeReasons.GetUniqueFlags().ToArray(); + if (!IsDirectStream && transcodeReasonsValues.Length > 0) + { + sb.Append("&TranscodeReasons="); + sb.AppendJoin(',', transcodeReasonsValues); + } + + if (!string.IsNullOrEmpty(query)) + { + sb.Append(query); + } + + return sb.ToString(); } /// diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index eff2e09da1..66de18cfe1 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -1,12 +1,10 @@ #nullable disable #pragma warning disable CS1591 -using System; using System.Collections.Generic; using System.ComponentModel; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; -using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; @@ -17,10 +15,10 @@ namespace MediaBrowser.Model.Dto { public MediaSourceInfo() { - Formats = Array.Empty(); - MediaStreams = Array.Empty(); - MediaAttachments = Array.Empty(); - RequiredHttpHeaders = new Dictionary(); + Formats = []; + MediaStreams = []; + MediaAttachments = []; + RequiredHttpHeaders = []; SupportsTranscoding = true; SupportsDirectStream = true; SupportsDirectPlay = true; diff --git a/src/Jellyfin.Extensions/EnumerableExtensions.cs b/src/Jellyfin.Extensions/EnumerableExtensions.cs index fd46358a4f..3eb9da01f2 100644 --- a/src/Jellyfin.Extensions/EnumerableExtensions.cs +++ b/src/Jellyfin.Extensions/EnumerableExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Jellyfin.Extensions; @@ -55,4 +56,22 @@ public static class EnumerableExtensions { yield return item; } + + /// + /// Gets an IEnumerable consisting of all flags of an enum. + /// + /// The flags enum. + /// The type of item. + /// The IEnumerable{Enum}. + public static IEnumerable GetUniqueFlags(this T flags) + where T : Enum + { + foreach (Enum value in Enum.GetValues(flags.GetType())) + { + if (flags.HasFlag(value)) + { + yield return (T)value; + } + } + } } diff --git a/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs b/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs new file mode 100644 index 0000000000..981287c033 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Jellyfin.Data.Enums; +using Jellyfin.Extensions; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; + +namespace Jellyfin.Model.Tests.Dlna; + +public class LegacyStreamInfo : StreamInfo +{ + public LegacyStreamInfo(Guid itemId, DlnaProfileType mediaType) + { + ItemId = itemId; + MediaType = mediaType; + } + + /// + /// The 10.6 ToUrl code from StreamInfo.cs with which to compare new version. + /// + /// The base url to use. + /// The Access token. + /// A url. + public string ToUrl_Original(string baseUrl, string? accessToken) + { + ArgumentException.ThrowIfNullOrEmpty(baseUrl); + + var list = new List(); + foreach (NameValuePair pair in BuildParams(this, accessToken)) + { + if (string.IsNullOrEmpty(pair.Value)) + { + continue; + } + + // Try to keep the url clean by omitting defaults + if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase) + && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal); + + list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue)); + } + + string queryString = string.Join('&', list); + + return GetUrl(baseUrl, queryString); + } + + private string GetUrl(string baseUrl, string queryString) + { + ArgumentException.ThrowIfNullOrEmpty(baseUrl); + + string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container; + + baseUrl = baseUrl.TrimEnd('/'); + + if (MediaType == DlnaProfileType.Audio) + { + if (SubProtocol == MediaStreamProtocol.hls) + { + return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + } + + return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + } + + if (SubProtocol == MediaStreamProtocol.hls) + { + return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString); + } + + return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString); + } + + private static List BuildParams(StreamInfo item, string? accessToken) + { + var list = new List(); + + string audioCodecs = item.AudioCodecs.Count == 0 ? + string.Empty : + string.Join(',', item.AudioCodecs); + + string videoCodecs = item.VideoCodecs.Count == 0 ? + string.Empty : + string.Join(',', item.VideoCodecs); + + list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty)); + list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty)); + list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty)); + list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + list.Add(new NameValuePair("VideoCodec", videoCodecs)); + list.Add(new NameValuePair("AudioCodec", audioCodecs)); + list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + long startPositionTicks = item.StartPositionTicks; + + if (item.SubProtocol == MediaStreamProtocol.hls) + { + list.Add(new NameValuePair("StartTimeTicks", string.Empty)); + list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty)); + + if (item.SegmentLength.HasValue) + { + list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture))); + } + + if (item.MinSegments.HasValue) + { + list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture))); + } + + list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture))); + } + else + { + list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture))); + } + + list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty)); + list.Add(new NameValuePair("ApiKey", accessToken ?? string.Empty)); + + string? liveStreamId = item.MediaSource?.LiveStreamId; + list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty)); + + if (!item.IsDirectStream) + { + if (item.RequireNonAnamorphic) + { + list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } + + list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + + if (item.EnableSubtitlesInManifest) + { + list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } + + if (item.EnableMpegtsM2TsMode) + { + list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } + + if (item.EstimateContentLength) + { + list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } + + if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto) + { + list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant())); + } + + if (item.CopyTimestamps) + { + list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } + + if (item.RequireAvc) + { + list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } + + if (item.EnableAudioVbrEncoding) + { + list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); + } + } + + list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty)); + + string subtitleCodecs = item.SubtitleCodecs.Count == 0 ? + string.Empty : + string.Join(",", item.SubtitleCodecs); + + list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); + list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); + + foreach (var pair in item.StreamOptions) + { + if (string.IsNullOrEmpty(pair.Value)) + { + continue; + } + + // strip spaces to avoid having to encode h264 profile names + list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal))); + } + + var transcodeReasonsValues = item.TranscodeReasons.GetUniqueFlags().ToArray(); + if (!item.IsDirectStream && transcodeReasonsValues.Length > 0) + { + list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString())); + } + + return list; + } +} diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index db9791263e..ae9edd3867 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -594,7 +594,7 @@ namespace Jellyfin.Model.Tests private static (string Path, NameValueCollection Query, string Filename, string Extension) ParseUri(StreamInfo val) { - var href = val.ToUrl("media:", "ACCESSTOKEN").Split("?", 2); + var href = val.ToUrl("media:", "ACCESSTOKEN", null).Split("?", 2); var path = href[0]; var queryString = href.ElementAtOrDefault(1); diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs new file mode 100644 index 0000000000..86819de8c0 --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Text; +using MediaBrowser.Model.Dlna; +using Xunit; + +namespace Jellyfin.Model.Tests.Dlna; + +public class StreamInfoTests +{ + private const string BaseUrl = "/test/"; + private const int RandomSeed = 298347823; + + /// + /// Returns a random float. + /// + /// The instance. + /// A random . + private static float RandomFloat(Random random) + { + var buffer = new byte[4]; + random.NextBytes(buffer); + return BitConverter.ToSingle(buffer, 0); + } + + /// + /// Creates a random array. + /// + /// The instance. + /// The element of the array. + /// An of . + private static object? RandomArray(Random random, Type? elementType) + { + if (elementType == null) + { + return null; + } + + if (elementType == typeof(string)) + { + return RandomStringArray(random); + } + + if (elementType == typeof(int)) + { + return RandomIntArray(random); + } + + if (elementType.IsEnum) + { + var values = Enum.GetValues(elementType); + return RandomIntArray(random, 0, values.Length - 1); + } + + throw new ArgumentException("Unsupported array type " + elementType.ToString()); + } + + /// + /// Creates a random length string. + /// + /// The instance. + /// The minimum length of the string. + /// The maximum length of the string. + /// The string. + private static string RandomString(Random random, int minLength = 0, int maxLength = 256) + { + var len = random.Next(minLength, maxLength); + var sb = new StringBuilder(len); + + while (len > 0) + { + sb.Append((char)random.Next(65, 97)); + len--; + } + + return sb.ToString(); + } + + /// + /// Creates a random long. + /// + /// The instance. + /// Min value. + /// Max value. + /// A random between and . + private static long RandomLong(Random random, long min = -9223372036854775808, long max = 9223372036854775807) + { + long result = random.Next((int)(min >> 32), (int)(max >> 32)); + result <<= 32; + result |= (long)random.Next((int)(min >> 32) << 32, (int)(max >> 32) << 32); + return result; + } + + /// + /// Creates a random string array containing between and . + /// + /// The instance. + /// The minimum number of elements. + /// The maximum number of elements. + /// A random instance. + private static string[] RandomStringArray(Random random, int minLength = 0, int maxLength = 9) + { + var len = random.Next(minLength, maxLength); + var arr = new List(len); + while (len > 0) + { + arr.Add(RandomString(random, 1, 30)); + len--; + } + + return arr.ToArray(); + } + + /// + /// Creates a random int array containing between and . + /// + /// The instance. + /// The minimum number of elements. + /// The maximum number of elements. + /// A random instance. + private static int[] RandomIntArray(Random random, int minLength = 0, int maxLength = 9) + { + var len = random.Next(minLength, maxLength); + var arr = new List(len); + while (len > 0) + { + arr.Add(random.Next()); + len--; + } + + return arr.ToArray(); + } + + /// + /// Fills most properties with random data. + /// + /// The instance to fill with data. + private static void FillAllProperties(T destination) + { + var random = new Random(RandomSeed); + var objectType = destination!.GetType(); + foreach (var property in objectType.GetProperties()) + { + if (!(property.CanRead && property.CanWrite)) + { + continue; + } + + var type = property.PropertyType; + // If nullable, then set it to null, 25% of the time. + if (Nullable.GetUnderlyingType(type) != null) + { + if (random.Next(0, 4) == 0) + { + // Set it to null. + property.SetValue(destination, null); + continue; + } + } + + if (type == typeof(Guid)) + { + property.SetValue(destination, Guid.NewGuid()); + continue; + } + + if (type.IsEnum) + { + Array values = Enum.GetValues(property.PropertyType); + property.SetValue(destination, values.GetValue(random.Next(0, values.Length - 1))); + continue; + } + + if (type == typeof(long)) + { + property.SetValue(destination, RandomLong(random)); + continue; + } + + if (type == typeof(string)) + { + property.SetValue(destination, RandomString(random)); + continue; + } + + if (type == typeof(bool)) + { + property.SetValue(destination, random.Next(0, 1) == 1); + continue; + } + + if (type == typeof(float)) + { + property.SetValue(destination, RandomFloat(random)); + continue; + } + + if (type.IsArray) + { + property.SetValue(destination, RandomArray(random, type.GetElementType())); + continue; + } + } + } + + [InlineData(DlnaProfileType.Audio)] + [InlineData(DlnaProfileType.Video)] + [InlineData(DlnaProfileType.Photo)] + [Theory] + public void Test_Blank_Url_Method(DlnaProfileType type) + { + var streamInfo = new LegacyStreamInfo(Guid.Empty, type) + { + DeviceProfile = new DeviceProfile() + }; + + string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123"); + + // New version will return and & after the ? due to optional parameters. + string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase); + + Assert.Equal(legacyUrl, newUrl, ignoreCase: true); + } + + [Fact] + public void Fuzzy_Comparison() + { + var streamInfo = new LegacyStreamInfo(Guid.Empty, DlnaProfileType.Video) + { + DeviceProfile = new DeviceProfile() + }; + for (int i = 0; i < 100000; i++) + { + FillAllProperties(streamInfo); + string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123"); + + // New version will return and & after the ? due to optional parameters. + string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase); + + Assert.Equal(legacyUrl, newUrl, ignoreCase: true); + } + } +} From 2c499d1e86f1ea3ee087bc8f6ff101a1b1ec2ab3 Mon Sep 17 00:00:00 2001 From: Johannes Heuel Date: Fri, 28 Mar 2025 13:54:12 +0100 Subject: [PATCH 144/508] feat: allow grouping shows into collections (#13236) * feat: allow grouping shows into collections * add pre-startup routine to rename EnableGroupingIntoCollections * Update Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs --- Jellyfin.Server/Migrations/MigrationRunner.cs | 3 +- .../RenameEnableGroupingIntoCollections.cs | 63 +++++++++++++++++++ MediaBrowser.Controller/Entities/Folder.cs | 12 ++-- MediaBrowser.Controller/Entities/TV/Series.cs | 2 +- .../Configuration/ServerConfiguration.cs | 4 +- 5 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index fd540c9c0d..9865199f3b 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -29,7 +29,8 @@ namespace Jellyfin.Server.Migrations typeof(PreStartupRoutines.CreateNetworkConfiguration), typeof(PreStartupRoutines.MigrateMusicBrainzTimeout), typeof(PreStartupRoutines.MigrateNetworkConfiguration), - typeof(PreStartupRoutines.MigrateEncodingOptions) + typeof(PreStartupRoutines.MigrateEncodingOptions), + typeof(PreStartupRoutines.RenameEnableGroupingIntoCollections) }; /// diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs new file mode 100644 index 0000000000..0a37b35a6a --- /dev/null +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using Emby.Server.Implementations; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.PreStartupRoutines; + +/// +public class RenameEnableGroupingIntoCollections : IMigrationRoutine +{ + private readonly ServerApplicationPaths _applicationPaths; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// An instance of the interface. + public RenameEnableGroupingIntoCollections(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory) + { + _applicationPaths = applicationPaths; + _logger = loggerFactory.CreateLogger(); + } + + /// + public Guid Id => Guid.Parse("E73B777D-CD5C-4E71-957A-B86B3660B7CF"); + + /// + public string Name => nameof(RenameEnableGroupingIntoCollections); + + /// + public bool PerformOnNewInstall => false; + + /// + public void Perform() + { + string path = Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "system.xml"); + if (!File.Exists(path)) + { + _logger.LogWarning("Configuration file not found: {Path}", path); + return; + } + + try + { + XDocument xmlDocument = XDocument.Load(path); + var element = xmlDocument.Descendants("EnableGroupingIntoCollections").FirstOrDefault(); + if (element is not null) + { + element.Name = "EnableGroupingMoviesIntoCollections"; + _logger.LogInformation("The tag was successfully renamed to ."); + xmlDocument.Save(path); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while updating the XML file: {Message}", ex.Message); + } + } +} diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index dd85a6ec0e..4da22854b2 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1064,11 +1064,6 @@ namespace MediaBrowser.Controller.Entities return false; } - if (queryParent is Series) - { - return false; - } - if (queryParent is Season) { return false; @@ -1088,12 +1083,15 @@ namespace MediaBrowser.Controller.Entities if (!param.HasValue) { - if (user is not null && !configurationManager.Configuration.EnableGroupingIntoCollections) + if (user is not null && query.IncludeItemTypes.Any(type => + (type == BaseItemKind.Movie && !configurationManager.Configuration.EnableGroupingMoviesIntoCollections) || + (type == BaseItemKind.Series && !configurationManager.Configuration.EnableGroupingShowsIntoCollections))) { return false; } - if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie)) + if (query.IncludeItemTypes.Length == 0 + || query.IncludeItemTypes.Any(type => type == BaseItemKind.Movie || type == BaseItemKind.Series)) { param = true; } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index 8d55576224..b4ad05921e 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Entities.TV /// /// Class Series. /// - public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo, IMetadataContainer + public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo, IMetadataContainer, ISupportsBoxSetGrouping { public Series() { diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index f4e6c8e2c5..a58c01c960 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -204,7 +204,9 @@ public class ServerConfiguration : BaseApplicationConfiguration public bool EnableFolderView { get; set; } = false; - public bool EnableGroupingIntoCollections { get; set; } = false; + public bool EnableGroupingMoviesIntoCollections { get; set; } = false; + + public bool EnableGroupingShowsIntoCollections { get; set; } = false; public bool DisplaySpecialsWithinSeasons { get; set; } = true; From 384134fd25fec22792733ccaa9f10a23d5c98eac Mon Sep 17 00:00:00 2001 From: gnattu Date: Fri, 28 Mar 2025 21:22:00 +0800 Subject: [PATCH 145/508] Use string literal --- .../Encoder/ApplePlatformHelper.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs index ea2289bd7d..76fc5f695f 100644 --- a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs +++ b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs @@ -14,7 +14,7 @@ public static class ApplePlatformHelper { private static readonly string[] _av1DecodeBlacklistedCpuClass = ["M1", "M2"]; - private static string GetSysctlValue(string name) + private static string GetSysctlValue(ReadOnlySpan name) { IntPtr length = IntPtr.Zero; // Get length of the value @@ -22,7 +22,7 @@ public static class ApplePlatformHelper if (osStatus != 0) { - throw new NotSupportedException($"Failed to get sysctl value for {name} with error {osStatus}"); + throw new NotSupportedException($"Failed to get sysctl value for {System.Text.Encoding.UTF8.GetString(name)} with error {osStatus}"); } IntPtr buffer = Marshal.AllocHGlobal(length.ToInt32()); @@ -31,7 +31,7 @@ public static class ApplePlatformHelper osStatus = SysctlByName(name, buffer, ref length, IntPtr.Zero, 0); if (osStatus != 0) { - throw new NotSupportedException($"Failed to get sysctl value for {name} with error {osStatus}"); + throw new NotSupportedException($"Failed to get sysctl value for {System.Text.Encoding.UTF8.GetString(name)} with error {osStatus}"); } return Marshal.PtrToStringAnsi(buffer) ?? string.Empty; @@ -42,9 +42,9 @@ public static class ApplePlatformHelper } } - private static int SysctlByName(string name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen) + private static int SysctlByName(ReadOnlySpan name, IntPtr oldp, ref IntPtr oldlenp, IntPtr newp, uint newlen) { - return NativeMethods.SysctlByName(System.Text.Encoding.ASCII.GetBytes(name), oldp, ref oldlenp, newp, newlen); + return NativeMethods.SysctlByName(name.ToArray(), oldp, ref oldlenp, newp, newlen); } /// @@ -61,7 +61,7 @@ public static class ApplePlatformHelper try { - string cpuBrandString = GetSysctlValue("machdep.cpu.brand_string"); + string cpuBrandString = GetSysctlValue("machdep.cpu.brand_string"u8); return !_av1DecodeBlacklistedCpuClass.Any(blacklistedCpuClass => cpuBrandString.Contains(blacklistedCpuClass, StringComparison.OrdinalIgnoreCase)); } catch (NotSupportedException e) From c69e9d8f2cac26a832f0ee09bed2809aae1872bf Mon Sep 17 00:00:00 2001 From: gnattu Date: Fri, 28 Mar 2025 21:30:39 +0800 Subject: [PATCH 146/508] Gate the macOS only functions --- MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs | 2 ++ MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs index 76fc5f695f..a8ff58b091 100644 --- a/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs +++ b/MediaBrowser.MediaEncoding/Encoder/ApplePlatformHelper.cs @@ -3,6 +3,7 @@ using System; using System.Linq; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Encoder; @@ -10,6 +11,7 @@ namespace MediaBrowser.MediaEncoding.Encoder; /// /// Helper class for Apple platform specific operations. /// +[SupportedOSPlatform("macos")] public static class ApplePlatformHelper { private static readonly string[] _av1DecodeBlacklistedCpuClass = ["M1", "M2"]; diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 776b2ab42c..54d0eb4b51 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Runtime.Versioning; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; @@ -437,6 +438,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + [SupportedOSPlatform("macos")] public bool CheckIsVideoToolboxAv1DecodeAvailable() { return ApplePlatformHelper.HasAv1HardwareAccel(_logger); From c906c1ca0d4ded3430f4328240f487ed962e9b32 Mon Sep 17 00:00:00 2001 From: LJQ Date: Sat, 29 Mar 2025 02:33:58 +0800 Subject: [PATCH 147/508] rebase --- MediaBrowser.Providers/Manager/MetadataService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index e8994693de..2cb3814095 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -780,7 +780,9 @@ namespace MediaBrowser.Providers.Manager } else { - var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata; + var shouldReplace = (options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly && options.ReplaceAllMetadata) + // Case for Scan for new and updated files + || (options.MetadataRefreshMode == MetadataRefreshMode.Default && !options.ReplaceAllMetadata); MergeData(temp, metadata, item.LockedFields, shouldReplace, true); } } From d75216cf3a9bb3f869415bed829dca996f4ee7c9 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 29 Mar 2025 17:00:19 +0100 Subject: [PATCH 148/508] Fixes cleanup of wrong table in migration (#13796) --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 427f04f9d8..cc90a53e8e 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -272,7 +272,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine peopleCache.Clear(); - _logger.LogInformation("Try saving {0} People entries.", dbContext.MediaStreamInfos.Local.Count); + _logger.LogInformation("Try saving {0} People entries.", dbContext.Peoples.Local.Count); dbContext.SaveChanges(); migrationTotalTime += stopwatch.Elapsed; _logger.LogInformation("Saving People entries took {0}.", stopwatch.Elapsed); @@ -305,7 +305,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine AND EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId) """; - dbContext.Chapters.ExecuteDelete(); + dbContext.AncestorIds.ExecuteDelete(); foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery)) { @@ -313,7 +313,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine dbContext.AncestorIds.Add(ancestorId); } - _logger.LogInformation("Try saving {0} AncestorIds entries.", dbContext.Chapters.Local.Count); + _logger.LogInformation("Try saving {0} AncestorIds entries.", dbContext.AncestorIds.Local.Count); dbContext.SaveChanges(); migrationTotalTime += stopwatch.Elapsed; From 90a6cca92bd9db17a4c264c7c0b064af6e0695af Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 31 Mar 2025 05:36:27 +0200 Subject: [PATCH 149/508] Library.db migration impovements (#13809) * Fixes cleanup of wrong table in migration * use dedicated context for each step * Use prepared Context * Fix measurement of UserData migration time * Update logging and combine cleanup to its own stage * fix people map not logging migrate only readonly database * Add id blacklisting in migration to avoid duplicated log entires --- .../Migrations/Routines/MigrateLibraryDb.cs | 531 +++++++++++------- 1 file changed, 320 insertions(+), 211 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index cc90a53e8e..f414b6e396 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -73,273 +73,328 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine var dataPath = _paths.DataPath; var libraryDbPath = Path.Combine(dataPath, DbFilename); - using var connection = new SqliteConnection($"Filename={libraryDbPath}"); - var migrationTotalTime = TimeSpan.Zero; + using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly"); - var stopwatch = new Stopwatch(); - stopwatch.Start(); + var fullOperationTimer = new Stopwatch(); + fullOperationTimer.Start(); - connection.Open(); - using var dbContext = _provider.CreateDbContext(); - - migrationTotalTime += stopwatch.Elapsed; - _logger.LogInformation("Saving UserData entries took {0}.", stopwatch.Elapsed); - stopwatch.Restart(); - - _logger.LogInformation("Start moving TypedBaseItem."); - const string typedBaseItemsQuery = """ - SELECT guid, type, data, StartDate, EndDate, ChannelId, IsMovie, - IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLanguage, - PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIndexNumber, - ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, Genres, ParentId, TopParentId, - Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, - DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId, - PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, - ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType FROM TypedBaseItems - """; - dbContext.BaseItems.ExecuteDelete(); + using (var operation = GetPreparedDbContext("Cleanup database")) + { + operation.JellyfinDbContext.BaseItems.ExecuteDelete(); + operation.JellyfinDbContext.ItemValues.ExecuteDelete(); + operation.JellyfinDbContext.UserData.ExecuteDelete(); + operation.JellyfinDbContext.MediaStreamInfos.ExecuteDelete(); + operation.JellyfinDbContext.Peoples.ExecuteDelete(); + operation.JellyfinDbContext.PeopleBaseItemMap.ExecuteDelete(); + operation.JellyfinDbContext.Chapters.ExecuteDelete(); + operation.JellyfinDbContext.AncestorIds.ExecuteDelete(); + } var legacyBaseItemWithUserKeys = new Dictionary(); - foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery)) + connection.Open(); + + var baseItemIds = new HashSet(); + using (var operation = GetPreparedDbContext("moving TypedBaseItem")) { - var baseItem = GetItem(dto); - dbContext.BaseItems.Add(baseItem.BaseItem); - foreach (var dataKey in baseItem.LegacyUserDataKey) + const string typedBaseItemsQuery = + """ + SELECT guid, type, data, StartDate, EndDate, ChannelId, IsMovie, + IsSeries, EpisodeTitle, IsRepeat, CommunityRating, CustomRating, IndexNumber, IsLocked, PreferredMetadataLanguage, + PreferredMetadataCountryCode, Width, Height, DateLastRefreshed, Name, Path, PremiereDate, Overview, ParentIndexNumber, + ProductionYear, OfficialRating, ForcedSortName, RunTimeTicks, Size, DateCreated, DateModified, Genres, ParentId, TopParentId, + Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, + DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId, + PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, + ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName, UnratedType FROM TypedBaseItems + """; + using (new TrackedMigrationStep("Loading TypedBaseItems", _logger)) { - legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem; + foreach (SqliteDataReader dto in connection.Query(typedBaseItemsQuery)) + { + var baseItem = GetItem(dto); + operation.JellyfinDbContext.BaseItems.Add(baseItem.BaseItem); + baseItemIds.Add(baseItem.BaseItem.Id); + foreach (var dataKey in baseItem.LegacyUserDataKey) + { + legacyBaseItemWithUserKeys[dataKey] = baseItem.BaseItem; + } + } + } + + using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.BaseItems.Local.Count} BaseItem entries", _logger)) + { + operation.JellyfinDbContext.SaveChanges(); } } - _logger.LogInformation("Try saving {0} BaseItem entries.", dbContext.BaseItems.Local.Count); - dbContext.SaveChanges(); - migrationTotalTime += stopwatch.Elapsed; - _logger.LogInformation("Saving BaseItems entries took {0}.", stopwatch.Elapsed); - stopwatch.Restart(); - - _logger.LogInformation("Start moving ItemValues."); - // do not migrate inherited types as they are now properly mapped in search and lookup. - const string itemValueQuery = - """ - SELECT ItemId, Type, Value, CleanValue FROM ItemValues - WHERE Type <> 6 AND EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = ItemValues.ItemId) - """; - dbContext.ItemValues.ExecuteDelete(); - - // EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow. - var localItems = new Dictionary<(int Type, string CleanValue), (Database.Implementations.Entities.ItemValue ItemValue, List ItemIds)>(); - - foreach (SqliteDataReader dto in connection.Query(itemValueQuery)) + using (var operation = GetPreparedDbContext("moving ItemValues")) { - var itemId = dto.GetGuid(0); - var entity = GetItemValue(dto); - var key = ((int)entity.Type, entity.CleanValue); - if (!localItems.TryGetValue(key, out var existing)) + // do not migrate inherited types as they are now properly mapped in search and lookup. + const string itemValueQuery = + """ + SELECT ItemId, Type, Value, CleanValue FROM ItemValues + WHERE Type <> 6 AND EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = ItemValues.ItemId) + """; + + // EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow. + var localItems = new Dictionary<(int Type, string CleanValue), (Database.Implementations.Entities.ItemValue ItemValue, List ItemIds)>(); + using (new TrackedMigrationStep("loading ItemValues", _logger)) { - localItems[key] = existing = (entity, []); + foreach (SqliteDataReader dto in connection.Query(itemValueQuery)) + { + var itemId = dto.GetGuid(0); + var entity = GetItemValue(dto); + var key = ((int)entity.Type, entity.CleanValue); + if (!localItems.TryGetValue(key, out var existing)) + { + localItems[key] = existing = (entity, []); + } + + existing.ItemIds.Add(itemId); + } + + foreach (var item in localItems) + { + operation.JellyfinDbContext.ItemValues.Add(item.Value.ItemValue); + operation.JellyfinDbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new ItemValueMap() + { + Item = null!, + ItemValue = null!, + ItemId = f, + ItemValueId = item.Value.ItemValue.ItemValueId + })); + } } - existing.ItemIds.Add(itemId); + using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.ItemValues.Local.Count} ItemValues entries", _logger)) + { + operation.JellyfinDbContext.SaveChanges(); + } } - foreach (var item in localItems) + using (var operation = GetPreparedDbContext("moving UserData")) { - dbContext.ItemValues.Add(item.Value.ItemValue); - dbContext.ItemValuesMap.AddRange(item.Value.ItemIds.Distinct().Select(f => new ItemValueMap() + var queryResult = connection.Query( + """ + SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas + + WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key) + """); + + using (new TrackedMigrationStep("loading UserData", _logger)) { - Item = null!, - ItemValue = null!, - ItemId = f, - ItemValueId = item.Value.ItemValue.ItemValueId - })); - } + var users = operation.JellyfinDbContext.Users.AsNoTracking().ToImmutableArray(); + var userIdBlacklist = new HashSet(); - _logger.LogInformation("Try saving {0} ItemValues entries.", dbContext.ItemValues.Local.Count); - dbContext.SaveChanges(); - migrationTotalTime += stopwatch.Elapsed; - _logger.LogInformation("Saving People ItemValues took {0}.", stopwatch.Elapsed); - stopwatch.Restart(); + foreach (var entity in queryResult) + { + var userData = GetUserData(users, entity, userIdBlacklist); + if (userData is null) + { + var userDataId = entity.GetString(0); + var internalUserId = entity.GetInt32(1); - _logger.LogInformation("Start moving UserData."); - var queryResult = connection.Query(""" - SELECT key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex FROM UserDatas + if (!userIdBlacklist.Contains(internalUserId)) + { + _logger.LogError("Was not able to migrate user data with key {0} because its id {InternalId} does not match any existing user.", userDataId, internalUserId); + userIdBlacklist.Add(internalUserId); + } - WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.UserDataKey = UserDatas.key) - """); + continue; + } - dbContext.UserData.ExecuteDelete(); + if (!legacyBaseItemWithUserKeys.TryGetValue(userData.CustomDataKey!, out var refItem)) + { + _logger.LogError("Was not able to migrate user data with key {0} because it does not reference a valid BaseItem.", entity.GetString(0)); + continue; + } - var users = dbContext.Users.AsNoTracking().ToImmutableArray(); + userData.ItemId = refItem.Id; + operation.JellyfinDbContext.UserData.Add(userData); + } - foreach (var entity in queryResult) - { - var userData = GetUserData(users, entity); - if (userData is null) - { - _logger.LogError("Was not able to migrate user data with key {0}", entity.GetString(0)); - continue; + users.Clear(); } - if (!legacyBaseItemWithUserKeys.TryGetValue(userData.CustomDataKey!, out var refItem)) + legacyBaseItemWithUserKeys.Clear(); + + using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.UserData.Local.Count} UserData entries", _logger)) { - _logger.LogError("Was not able to migrate user data with key {0} because it does not reference a valid BaseItem.", entity.GetString(0)); - continue; + operation.JellyfinDbContext.SaveChanges(); + } + } + + using (var operation = GetPreparedDbContext("moving MediaStreamInfos")) + { + const string mediaStreamQuery = + """ + SELECT ItemId, StreamIndex, StreamType, Codec, Language, ChannelLayout, Profile, AspectRatio, Path, + IsInterlaced, BitRate, Channels, SampleRate, IsDefault, IsForced, IsExternal, Height, Width, + AverageFrameRate, RealFrameRate, Level, PixelFormat, BitDepth, IsAnamorphic, RefFrames, CodecTag, + Comment, NalLengthSize, IsAvc, Title, TimeBase, CodecTimeBase, ColorPrimaries, ColorSpace, ColorTransfer, + DvVersionMajor, DvVersionMinor, DvProfile, DvLevel, RpuPresentFlag, ElPresentFlag, BlPresentFlag, DvBlSignalCompatibilityId, IsHearingImpaired + FROM MediaStreams + WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId) + """; + + using (new TrackedMigrationStep("loading MediaStreamInfos", _logger)) + { + foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery)) + { + operation.JellyfinDbContext.MediaStreamInfos.Add(GetMediaStream(dto)); + } } - userData.ItemId = refItem.Id; - dbContext.UserData.Add(userData); - } - - users.Clear(); - legacyBaseItemWithUserKeys.Clear(); - _logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count); - dbContext.SaveChanges(); - - _logger.LogInformation("Start moving MediaStreamInfos."); - const string mediaStreamQuery = """ - SELECT ItemId, StreamIndex, StreamType, Codec, Language, ChannelLayout, Profile, AspectRatio, Path, - IsInterlaced, BitRate, Channels, SampleRate, IsDefault, IsForced, IsExternal, Height, Width, - AverageFrameRate, RealFrameRate, Level, PixelFormat, BitDepth, IsAnamorphic, RefFrames, CodecTag, - Comment, NalLengthSize, IsAvc, Title, TimeBase, CodecTimeBase, ColorPrimaries, ColorSpace, ColorTransfer, - DvVersionMajor, DvVersionMinor, DvProfile, DvLevel, RpuPresentFlag, ElPresentFlag, BlPresentFlag, DvBlSignalCompatibilityId, IsHearingImpaired - FROM MediaStreams - WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = MediaStreams.ItemId) - """; - dbContext.MediaStreamInfos.ExecuteDelete(); - - foreach (SqliteDataReader dto in connection.Query(mediaStreamQuery)) - { - dbContext.MediaStreamInfos.Add(GetMediaStream(dto)); - } - - _logger.LogInformation("Try saving {0} MediaStreamInfos entries.", dbContext.MediaStreamInfos.Local.Count); - dbContext.SaveChanges(); - - migrationTotalTime += stopwatch.Elapsed; - _logger.LogInformation("Saving MediaStreamInfos entries took {0}.", stopwatch.Elapsed); - stopwatch.Restart(); - - _logger.LogInformation("Start moving People."); - const string personsQuery = """ - SELECT ItemId, Name, Role, PersonType, SortOrder FROM People - WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId) - """; - dbContext.Peoples.ExecuteDelete(); - dbContext.PeopleBaseItemMap.ExecuteDelete(); - - var peopleCache = new Dictionary Items)>(); - var baseItemIds = dbContext.BaseItems.Select(b => b.Id).ToHashSet(); - - foreach (SqliteDataReader reader in connection.Query(personsQuery)) - { - var itemId = reader.GetGuid(0); - if (!baseItemIds.Contains(itemId)) + using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.MediaStreamInfos.Local.Count} MediaStreamInfos entries", _logger)) { - _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1)); - continue; + operation.JellyfinDbContext.SaveChanges(); + } + } + + using (var operation = GetPreparedDbContext("moving People")) + { + const string personsQuery = + """ + SELECT ItemId, Name, Role, PersonType, SortOrder FROM People + WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = People.ItemId) + """; + + var peopleCache = new Dictionary Items)>(); + + using (new TrackedMigrationStep("loading People", _logger)) + { + foreach (SqliteDataReader reader in connection.Query(personsQuery)) + { + var itemId = reader.GetGuid(0); + if (!baseItemIds.Contains(itemId)) + { + _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1)); + continue; + } + + var entity = GetPerson(reader); + if (!peopleCache.TryGetValue(entity.Name, out var personCache)) + { + peopleCache[entity.Name] = personCache = (entity, []); + } + + if (reader.TryGetString(2, out var role)) + { + } + + int? sortOrder = reader.IsDBNull(4) ? null : reader.GetInt32(4); + + personCache.Items.Add(new PeopleBaseItemMap() + { + Item = null!, + ItemId = itemId, + People = null!, + PeopleId = personCache.Person.Id, + ListOrder = sortOrder, + SortOrder = sortOrder, + Role = role + }); + } + + baseItemIds.Clear(); + + foreach (var item in peopleCache) + { + operation.JellyfinDbContext.Peoples.Add(item.Value.Person); + operation.JellyfinDbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId))); + } + + peopleCache.Clear(); } - var entity = GetPerson(reader); - if (!peopleCache.TryGetValue(entity.Name, out var personCache)) + using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Peoples.Local.Count} People entries and {operation.JellyfinDbContext.PeopleBaseItemMap.Local.Count} maps", _logger)) { - peopleCache[entity.Name] = personCache = (entity, []); + operation.JellyfinDbContext.SaveChanges(); + } + } + + using (var operation = GetPreparedDbContext("moving Chapters")) + { + const string chapterQuery = + """ + SELECT ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2 + WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId) + """; + + using (new TrackedMigrationStep("loading Chapters", _logger)) + { + foreach (SqliteDataReader dto in connection.Query(chapterQuery)) + { + var chapter = GetChapter(dto); + operation.JellyfinDbContext.Chapters.Add(chapter); + } } - if (reader.TryGetString(2, out var role)) + using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.Chapters.Local.Count} Chapters entries", _logger)) { + operation.JellyfinDbContext.SaveChanges(); + } + } + + using (var operation = GetPreparedDbContext("moving AncestorIds")) + { + const string ancestorIdsQuery = + """ + SELECT ItemId, AncestorId, AncestorIdText FROM AncestorIds + WHERE + EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.ItemId) + AND + EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId) + """; + + using (new TrackedMigrationStep("loading AncestorIds", _logger)) + { + foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery)) + { + var ancestorId = GetAncestorId(dto); + operation.JellyfinDbContext.AncestorIds.Add(ancestorId); + } } - int? sortOrder = reader.IsDBNull(4) ? null : reader.GetInt32(4); - - personCache.Items.Add(new PeopleBaseItemMap() + using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AncestorIds.Local.Count} AncestorId entries", _logger)) { - Item = null!, - ItemId = itemId, - People = null!, - PeopleId = personCache.Person.Id, - ListOrder = sortOrder, - SortOrder = sortOrder, - Role = role - }); + operation.JellyfinDbContext.SaveChanges(); + } } - baseItemIds.Clear(); - - foreach (var item in peopleCache) - { - dbContext.Peoples.Add(item.Value.Person); - dbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId))); - } - - peopleCache.Clear(); - - _logger.LogInformation("Try saving {0} People entries.", dbContext.Peoples.Local.Count); - dbContext.SaveChanges(); - migrationTotalTime += stopwatch.Elapsed; - _logger.LogInformation("Saving People entries took {0}.", stopwatch.Elapsed); - stopwatch.Restart(); - - _logger.LogInformation("Start moving Chapters."); - const string chapterQuery = """ - SELECT ItemId,StartPositionTicks,Name,ImagePath,ImageDateModified,ChapterIndex from Chapters2 - WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = Chapters2.ItemId) - """; - dbContext.Chapters.ExecuteDelete(); - - foreach (SqliteDataReader dto in connection.Query(chapterQuery)) - { - var chapter = GetChapter(dto); - dbContext.Chapters.Add(chapter); - } - - _logger.LogInformation("Try saving {0} Chapters entries.", dbContext.Chapters.Local.Count); - dbContext.SaveChanges(); - migrationTotalTime += stopwatch.Elapsed; - _logger.LogInformation("Saving Chapters took {0}.", stopwatch.Elapsed); - stopwatch.Restart(); - - _logger.LogInformation("Start moving AncestorIds."); - const string ancestorIdsQuery = """ - SELECT ItemId, AncestorId, AncestorIdText FROM AncestorIds - WHERE - EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.ItemId) - AND - EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = AncestorIds.AncestorId) - """; - dbContext.AncestorIds.ExecuteDelete(); - - foreach (SqliteDataReader dto in connection.Query(ancestorIdsQuery)) - { - var ancestorId = GetAncestorId(dto); - dbContext.AncestorIds.Add(ancestorId); - } - - _logger.LogInformation("Try saving {0} AncestorIds entries.", dbContext.AncestorIds.Local.Count); - - dbContext.SaveChanges(); - migrationTotalTime += stopwatch.Elapsed; - _logger.LogInformation("Saving AncestorIds took {0}.", stopwatch.Elapsed); - stopwatch.Restart(); - connection.Close(); + _logger.LogInformation("Migration of the Library.db done."); - _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old"); + _logger.LogInformation("Migrating Library db took {0}.", fullOperationTimer.Elapsed); SqliteConnection.ClearAllPools(); + _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old"); File.Move(libraryDbPath, libraryDbPath + ".old", true); - _logger.LogInformation("Migrating Library db took {0}.", migrationTotalTime); - _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); } - private UserData? GetUserData(ImmutableArray users, SqliteDataReader dto) + private DatabaseMigrationStep GetPreparedDbContext(string operationName) + { + var dbContext = _provider.CreateDbContext(); + dbContext.ChangeTracker.AutoDetectChangesEnabled = false; + dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + return new DatabaseMigrationStep(dbContext, operationName, _logger); + } + + private UserData? GetUserData(ImmutableArray users, SqliteDataReader dto, HashSet userIdBlacklist) { var internalUserId = dto.GetInt32(1); var user = users.FirstOrDefault(e => e.InternalId == internalUserId); if (user is null) { + if (userIdBlacklist.Contains(internalUserId)) + { + return null; + } + _logger.LogError("Tried to find user with index '{Idx}' but there are only '{MaxIdx}' users.", internalUserId, users.Length); return null; } @@ -1214,4 +1269,58 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine return image; } + + private class TrackedMigrationStep : IDisposable + { + private readonly string _operationName; + private readonly ILogger _logger; + private readonly Stopwatch _operationTimer; + private bool _disposed; + + public TrackedMigrationStep(string operationName, ILogger logger) + { + _operationName = operationName; + _logger = logger; + _operationTimer = Stopwatch.StartNew(); + logger.LogInformation("Start {OperationName}", operationName); + } + + public bool Disposed + { + get => _disposed; + set => _disposed = value; + } + + public virtual void Dispose() + { + if (Disposed) + { + return; + } + + Disposed = true; + _logger.LogInformation("{OperationName} took '{Time}'", _operationName, _operationTimer.Elapsed); + } + } + + private sealed class DatabaseMigrationStep : TrackedMigrationStep + { + public DatabaseMigrationStep(JellyfinDbContext jellyfinDbContext, string operationName, ILogger logger) : base(operationName, logger) + { + JellyfinDbContext = jellyfinDbContext; + } + + public JellyfinDbContext JellyfinDbContext { get; } + + public override void Dispose() + { + if (Disposed) + { + return; + } + + JellyfinDbContext.Dispose(); + base.Dispose(); + } + } } From 824bafc32dfe467ddfe161110b8974c3114d7367 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Mon, 31 Mar 2025 05:37:58 +0200 Subject: [PATCH 150/508] Fix StreamInfo.ToUrl (#13808) --- MediaBrowser.Model/Dlna/StreamInfo.cs | 20 ++++++------------ .../Dlna/LegacyStreamInfo.cs | 21 ++++++------------- 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index f9aab2d676..d89386c1ca 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -956,12 +956,10 @@ public class StreamInfo sb.Append(AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture)); } - if (SubtitleStreamIndex.HasValue && SubtitleDeliveryMethod != SubtitleDeliveryMethod.External && SubtitleStreamIndex != -1) + if (SubtitleStreamIndex.HasValue && (AlwaysBurnInSubtitleWhenTranscoding || SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) && SubtitleStreamIndex != -1) { sb.Append("&SubtitleStreamIndex="); sb.Append(SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture)); - sb.Append("&SubtitleMethod="); - sb.Append(SubtitleDeliveryMethod.ToString()); } if (VideoBitrate.HasValue) @@ -1095,17 +1093,11 @@ public class StreamInfo sb.Append(CopyTimestamps.ToString(CultureInfo.InvariantCulture)); } - if (RequireAvc) - { - sb.Append("&RequireAvc="); - sb.Append(RequireAvc.ToString(CultureInfo.InvariantCulture)); - } + sb.Append("&RequireAvc="); + sb.Append(RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); - if (EnableAudioVbrEncoding) - { - sb.Append("EnableAudioVbrEncoding="); - sb.Append(EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); - } + sb.Append("&EnableAudioVbrEncoding="); + sb.Append(EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); } var etag = MediaSource?.ETag; @@ -1118,7 +1110,7 @@ public class StreamInfo if (SubtitleStreamIndex.HasValue && SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) { sb.Append("&SubtitleMethod="); - sb.AppendJoin(',', SubtitleDeliveryMethod); + sb.Append(SubtitleDeliveryMethod); } if (SubtitleStreamIndex.HasValue && SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed && SubtitleCodecs.Count != 0) diff --git a/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs b/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs index 981287c033..e32baef55d 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/LegacyStreamInfo.cs @@ -92,7 +92,7 @@ public class LegacyStreamInfo : StreamInfo private static List BuildParams(StreamInfo item, string? accessToken) { - var list = new List(); + List list = []; string audioCodecs = item.AudioCodecs.Count == 0 ? string.Empty : @@ -109,7 +109,7 @@ public class LegacyStreamInfo : StreamInfo list.Add(new NameValuePair("VideoCodec", videoCodecs)); list.Add(new NameValuePair("AudioCodec", audioCodecs)); list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); - list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); + list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && (item.AlwaysBurnInSubtitleWhenTranscoding || item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty)); @@ -182,25 +182,16 @@ public class LegacyStreamInfo : StreamInfo list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } - if (item.RequireAvc) - { - list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } + list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - if (item.EnableAudioVbrEncoding) - { - list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); - } + list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant())); } list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty)); - string subtitleCodecs = item.SubtitleCodecs.Count == 0 ? - string.Empty : - string.Join(",", item.SubtitleCodecs); - - list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); + string subtitleCodecs = item.SubtitleCodecs.Count == 0 ? string.Empty : string.Join(",", item.SubtitleCodecs); list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty)); + list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty)); foreach (var pair in item.StreamOptions) { From 150094e3a43fad7a056b5bfdb0906a7aa225fdd1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 30 Mar 2025 21:38:36 -0600 Subject: [PATCH 151/508] Update dependency z440.atl.core to 6.20.0 (#13811) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f9e111b389..c81982aa8b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -80,7 +80,7 @@ - + From d7b786e7778c42a02b9fad2024ce4eea9af035a6 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Mon, 31 Mar 2025 05:38:50 +0200 Subject: [PATCH 152/508] Fix MoveTrickplayFiles migration (#13807) --- Jellyfin.Server/Migrations/MigrationRunner.cs | 2 +- Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs | 3 +-- .../Migrations/Routines/RemoveDuplicatePlaylistChildren.cs | 5 ----- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 9865199f3b..49568b087a 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -53,9 +53,9 @@ namespace Jellyfin.Server.Migrations typeof(Routines.AddDefaultCastReceivers), typeof(Routines.UpdateDefaultPluginRepository), typeof(Routines.FixAudioData), - typeof(Routines.MoveTrickplayFiles), typeof(Routines.RemoveDuplicatePlaylistChildren), typeof(Routines.MigrateLibraryDb), + typeof(Routines.MoveTrickplayFiles), }; /// diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs index f4ebac3778..eeb11e14c1 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.IO; using System.Linq; using Jellyfin.Data.Enums; -using MediaBrowser.Common; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Trickplay; @@ -16,7 +15,7 @@ namespace Jellyfin.Server.Migrations.Routines; /// /// Migration to move trickplay files to the new directory. /// -public class MoveTrickplayFiles : IMigrationRoutine +public class MoveTrickplayFiles : IDatabaseMigrationRoutine { private readonly ITrickplayManager _trickplayManager; private readonly IFileSystem _fileSystem; diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs index f84bccc258..e183a1d63a 100644 --- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs @@ -1,12 +1,10 @@ using System; using System.Linq; using System.Threading; - using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; -using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Routines; @@ -15,16 +13,13 @@ namespace Jellyfin.Server.Migrations.Routines; /// internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine { - private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IPlaylistManager _playlistManager; public RemoveDuplicatePlaylistChildren( - ILogger logger, ILibraryManager libraryManager, IPlaylistManager playlistManager) { - _logger = logger; _libraryManager = libraryManager; _playlistManager = playlistManager; } From 2ace8803453b235b0b0ae29f0069f9e5a3c069c8 Mon Sep 17 00:00:00 2001 From: Quyet Vu <72632257+quyet-v@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:39:51 +1300 Subject: [PATCH 153/508] Fix playlist order (#13730) * Fix playlist order move * Remove extra space * Added more test cases * Change namespace to file-scoped --- .../Playlists/PlaylistManager.cs | 12 +++++- .../Playlists/PlaylistManagerTests.cs | 40 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/Jellyfin.Server.Implementations.Tests/Playlists/PlaylistManagerTests.cs diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 7b0a164414..98a43b6c98 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -283,6 +283,16 @@ namespace Emby.Server.Implementations.Playlists RefreshPriority.High); } + internal static int DetermineAdjustedIndex(int newPriorIndexAllChildren, int newIndex) + { + if (newIndex == 0) + { + return newPriorIndexAllChildren > 0 ? newPriorIndexAllChildren - 1 : 0; + } + + return newPriorIndexAllChildren + 1; + } + public async Task MoveItemAsync(string playlistId, string entryId, int newIndex, Guid callingUserId) { if (_libraryManager.GetItemById(playlistId) is not Playlist playlist) @@ -305,7 +315,7 @@ namespace Emby.Server.Implementations.Playlists var newPriorItemIndex = newIndex > oldIndexAccessible ? newIndex : newIndex - 1 < 0 ? 0 : newIndex - 1; var newPriorItemId = accessibleChildren[newPriorItemIndex].Item1.ItemId; var newPriorItemIndexOnAllChildren = children.FindIndex(c => c.Item1.ItemId.Equals(newPriorItemId)); - var adjustedNewIndex = newPriorItemIndexOnAllChildren + 1; + var adjustedNewIndex = DetermineAdjustedIndex(newPriorItemIndexOnAllChildren, newIndex); var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)); if (item is null) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Playlists/PlaylistManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Playlists/PlaylistManagerTests.cs new file mode 100644 index 0000000000..cc8ca720e7 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Playlists/PlaylistManagerTests.cs @@ -0,0 +1,40 @@ +using Emby.Server.Implementations.Playlists; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Playlists; + +public class PlaylistManagerTests +{ + [Fact] + public void DetermineAdjustedIndexMoveToFirstPositionNoPriorInAllList() + { + var priorIndexAllChildren = 0; + var newIndex = 0; + + var adjustedIndex = PlaylistManager.DetermineAdjustedIndex(priorIndexAllChildren, newIndex); + + Assert.Equal(0, adjustedIndex); + } + + [Fact] + public void DetermineAdjustedIndexPriorInMiddleOfAllList() + { + var priorIndexAllChildren = 2; + var newIndex = 0; + + var adjustedIndex = PlaylistManager.DetermineAdjustedIndex(priorIndexAllChildren, newIndex); + + Assert.Equal(1, adjustedIndex); + } + + [Fact] + public void DetermineAdjustedIndexMoveMiddleOfPlaylist() + { + var priorIndexAllChildren = 2; + var newIndex = 1; + + var adjustedIndex = PlaylistManager.DetermineAdjustedIndex(priorIndexAllChildren, newIndex); + + Assert.Equal(3, adjustedIndex); + } +} From 3fc3b04daf929d1d3a9533fc410cb77885eb2e8a Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Mon, 31 Mar 2025 05:51:54 +0200 Subject: [PATCH 154/508] Rework parental ratings (#12615) --- .../Emby.Server.Implementations.csproj | 2 +- .../Library/SplashscreenPostScanTask.cs | 13 +- .../Localization/LocalizationManager.cs | 168 +- .../Localization/Ratings/0-prefer.csv | 11 - .../Localization/Ratings/0-prefer.json | 34 + .../Localization/Ratings/au.csv | 17 - .../Localization/Ratings/au.json | 69 + .../Localization/Ratings/be.csv | 11 - .../Localization/Ratings/be.json | 55 + .../Localization/Ratings/br.csv | 14 - .../Localization/Ratings/br.json | 55 + .../Localization/Ratings/ca.csv | 18 - .../Localization/Ratings/ca.json | 90 + .../Localization/Ratings/cl.json | 41 + .../Localization/Ratings/co.csv | 7 - .../Localization/Ratings/co.json | 55 + .../Localization/Ratings/de.csv | 17 - .../Localization/Ratings/de.json | 41 + .../Localization/Ratings/dk.csv | 7 - .../Localization/Ratings/dk.json | 48 + .../Localization/Ratings/es.csv | 25 - .../Localization/Ratings/es.json | 90 + .../Localization/Ratings/fi.csv | 10 - .../Localization/Ratings/fi.json | 41 + .../Localization/Ratings/fr.csv | 13 - .../Localization/Ratings/fr.json | 69 + .../Localization/Ratings/gb.csv | 23 - .../Localization/Ratings/gb.json | 97 + .../Localization/Ratings/ie.csv | 10 - .../Localization/Ratings/ie.json | 55 + .../Localization/Ratings/jp.csv | 11 - .../Localization/Ratings/jp.json | 62 + .../Localization/Ratings/kz.csv | 6 - .../Localization/Ratings/kz.json | 41 + .../Localization/Ratings/mx.csv | 6 - .../Localization/Ratings/mx.json | 41 + .../Localization/Ratings/nl.csv | 8 - .../Localization/Ratings/nl.json | 55 + .../Localization/Ratings/no.csv | 10 - .../Localization/Ratings/no.json | 69 + .../Localization/Ratings/nz.csv | 16 - .../Localization/Ratings/nz.json | 69 + .../Localization/Ratings/ro.csv | 6 - .../Localization/Ratings/ro.json | 48 + .../Localization/Ratings/ru.csv | 6 - .../Localization/Ratings/ru.json | 48 + .../Localization/Ratings/se.csv | 10 - .../Localization/Ratings/se.json | 55 + .../Localization/Ratings/sk.csv | 6 - .../Localization/Ratings/sk.json | 41 + .../Localization/Ratings/uk.csv | 22 - .../Localization/Ratings/uk.json | 97 + .../Localization/Ratings/us.csv | 52 - .../Localization/Ratings/us.json | 83 + .../Sorting/OfficialRatingComparer.cs | 71 +- Jellyfin.Api/Controllers/ItemsController.cs | 4 +- .../Controllers/LocalizationController.cs | 5 +- .../Extensions/ExpressionExtensions.cs | 70 + .../Item/BaseItemRepository.cs | 83 +- .../Users/UserManager.cs | 6 +- Jellyfin.Server/Migrations/MigrationRunner.cs | 2 +- .../Routines/MigrateRatingLevels.cs | 90 +- .../Migrations/Routines/MigrateUserDb.cs | 3 +- MediaBrowser.Controller/Entities/BaseItem.cs | 52 +- .../Entities/InternalItemsQuery.cs | 19 +- MediaBrowser.Model/Dto/MetadataEditorInfo.cs | 70 +- MediaBrowser.Model/Entities/ParentalRating.cs | 59 +- .../Entities/ParentalRatingEntry.cs | 22 + .../Entities/ParentalRatingScore.cs | 32 + .../Entities/ParentalRatingSystem.cs | 28 + .../Globalization/ILocalizationManager.cs | 101 +- MediaBrowser.Model/Querying/ItemFields.cs | 1 + MediaBrowser.Model/Users/UserPolicy.cs | 2 + .../Manager/MetadataService.cs | 1 + .../Entities/BaseItemEntity.cs | 2 + .../Entities/User.cs | 9 +- ...nheritedParentalRatingSubValue.Designer.cs | 1658 +++++++++++++++++ ...5026_AddInheritedParentalRatingSubValue.cs | 48 + .../Migrations/JellyfinDbModelSnapshot.cs | Bin 54724 -> 58741 bytes .../Localization/LocalizationManagerTests.cs | 66 +- 80 files changed, 3852 insertions(+), 726 deletions(-) delete mode 100644 Emby.Server.Implementations/Localization/Ratings/0-prefer.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/0-prefer.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/au.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/au.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/be.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/be.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/br.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/br.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/ca.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/ca.json create mode 100644 Emby.Server.Implementations/Localization/Ratings/cl.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/co.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/co.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/de.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/de.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/dk.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/dk.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/es.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/es.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/fi.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/fi.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/fr.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/fr.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/gb.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/gb.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/ie.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/ie.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/jp.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/jp.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/kz.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/kz.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/mx.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/mx.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/nl.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/nl.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/no.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/no.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/nz.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/nz.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/ro.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/ro.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/ru.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/ru.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/se.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/se.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/sk.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/sk.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/uk.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/uk.json delete mode 100644 Emby.Server.Implementations/Localization/Ratings/us.csv create mode 100644 Emby.Server.Implementations/Localization/Ratings/us.json create mode 100644 Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs create mode 100644 MediaBrowser.Model/Entities/ParentalRatingEntry.cs create mode 100644 MediaBrowser.Model/Entities/ParentalRatingScore.cs create mode 100644 MediaBrowser.Model/Entities/ParentalRatingSystem.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 6722c20da6..d99923b4fc 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -68,6 +68,6 @@ - + diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs index 0c9edd8398..71ce3b6012 100644 --- a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs +++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs @@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library; @@ -78,15 +77,15 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask CollapseBoxSetItems = false, Recursive = true, DtoOptions = new DtoOptions(false), - ImageTypes = new[] { imageType }, + ImageTypes = [imageType], Limit = 30, // TODO max parental rating configurable - MaxParentalRating = 10, - OrderBy = new[] - { + MaxParentalRating = new(10, null), + OrderBy = + [ (ItemSortBy.Random, SortOrder.Ascending) - }, - IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series } + ], + IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series] }); } } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 754a01329b..9598f9e6c7 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -26,20 +25,18 @@ namespace Emby.Server.Implementations.Localization private const string CulturesPath = "Emby.Server.Implementations.Localization.iso6392.txt"; private const string CountriesPath = "Emby.Server.Implementations.Localization.countries.json"; private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly; - private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated", "nr" }; + private static readonly string[] _unratedValues = ["n/a", "unrated", "not rated", "nr"]; private readonly IServerConfigurationManager _configurationManager; private readonly ILogger _logger; - private readonly Dictionary> _allParentalRatings = - new Dictionary>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _allParentalRatings = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary> _dictionaries = - new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary> _dictionaries = new(StringComparer.OrdinalIgnoreCase); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private List _cultures = new List(); + private List _cultures = []; /// /// Initializes a new instance of the class. @@ -68,35 +65,26 @@ namespace Emby.Server.Implementations.Localization continue; } - string countryCode = resource.Substring(RatingsPath.Length, 2); - var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); - - var stream = _assembly.GetManifestResourceStream(resource); - await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames() + using var stream = _assembly.GetManifestResourceStream(resource); + if (stream is not null) { - using var reader = new StreamReader(stream!); - await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) + var ratingSystem = await JsonSerializer.DeserializeAsync(stream, _jsonOptions).ConfigureAwait(false) + ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"); + + var dict = new Dictionary(); + if (ratingSystem.Ratings is not null) { - if (string.IsNullOrWhiteSpace(line)) + foreach (var ratingEntry in ratingSystem.Ratings) { - continue; + foreach (var ratingString in ratingEntry.RatingStrings) + { + dict[ratingString] = ratingEntry.RatingScore; + } } - string[] parts = line.Split(','); - if (parts.Length == 2 - && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - var name = parts[0]; - dict.Add(name, new ParentalRating(name, value)); - } - else - { - _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); - } + _allParentalRatings[ratingSystem.CountryCode] = dict; } } - - _allParentalRatings[countryCode] = dict; } await LoadCultures().ConfigureAwait(false); @@ -111,22 +99,29 @@ namespace Emby.Server.Implementations.Localization private async Task LoadCultures() { - List list = new List(); + List list = []; - await using var stream = _assembly.GetManifestResourceStream(CulturesPath) - ?? throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'"); - using var reader = new StreamReader(stream); - await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) + using var stream = _assembly.GetManifestResourceStream(CulturesPath); + if (stream is null) { - if (string.IsNullOrWhiteSpace(line)) + throw new InvalidOperationException($"Invalid resource path: '{CulturesPath}'"); + } + else + { + using var reader = new StreamReader(stream); + await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) { - continue; - } + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } - var parts = line.Split('|'); + var parts = line.Split('|'); + if (parts.Length != 5) + { + throw new InvalidDataException($"Invalid culture data found at: '{line}'"); + } - if (parts.Length == 5) - { string name = parts[3]; if (string.IsNullOrWhiteSpace(name)) { @@ -139,21 +134,21 @@ namespace Emby.Server.Implementations.Localization continue; } - string[] threeletterNames; + string[] threeLetterNames; if (string.IsNullOrWhiteSpace(parts[1])) { - threeletterNames = new[] { parts[0] }; + threeLetterNames = [parts[0]]; } else { - threeletterNames = new[] { parts[0], parts[1] }; + threeLetterNames = [parts[0], parts[1]]; } - list.Add(new CultureDto(name, name, twoCharName, threeletterNames)); + list.Add(new CultureDto(name, name, twoCharName, threeLetterNames)); } - } - _cultures = list; + _cultures = list; + } } /// @@ -176,82 +171,80 @@ namespace Emby.Server.Implementations.Localization } /// - public IEnumerable GetCountries() + public IReadOnlyList GetCountries() { - using StreamReader reader = new StreamReader( - _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'")); - return JsonSerializer.Deserialize>(reader.ReadToEnd(), _jsonOptions) - ?? throw new InvalidOperationException($"Resource contains invalid data: '{CountriesPath}'"); + using var stream = _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'"); + + return JsonSerializer.Deserialize>(stream, _jsonOptions) ?? []; } /// - public IEnumerable GetParentalRatings() + public IReadOnlyList GetParentalRatings() { // Use server default language for ratings // Fall back to empty list if there are no parental ratings for that language - var ratings = GetParentalRatingsDictionary()?.Values.ToList() - ?? new List(); + var ratings = GetParentalRatingsDictionary()?.Select(x => new ParentalRating(x.Key, x.Value)).ToList() ?? []; // Add common ratings to ensure them being available for selection // Based on the US rating system due to it being the main source of rating in the metadata providers // Unrated - if (!ratings.Any(x => x.Value is null)) + if (!ratings.Any(x => x is null)) { - ratings.Add(new ParentalRating("Unrated", null)); + ratings.Add(new("Unrated", null)); } // Minimum rating possible - if (ratings.All(x => x.Value != 0)) + if (ratings.All(x => x.RatingScore?.Score != 0)) { - ratings.Add(new ParentalRating("Approved", 0)); + ratings.Add(new("Approved", new(0, null))); } // Matches PG (this has different age restrictions depending on country) - if (ratings.All(x => x.Value != 10)) + if (ratings.All(x => x.RatingScore?.Score != 10)) { - ratings.Add(new ParentalRating("10", 10)); + ratings.Add(new("10", new(10, null))); } // Matches PG-13 - if (ratings.All(x => x.Value != 13)) + if (ratings.All(x => x.RatingScore?.Score != 13)) { - ratings.Add(new ParentalRating("13", 13)); + ratings.Add(new("13", new(13, null))); } // Matches TV-14 - if (ratings.All(x => x.Value != 14)) + if (ratings.All(x => x.RatingScore?.Score != 14)) { - ratings.Add(new ParentalRating("14", 14)); + ratings.Add(new("14", new(14, null))); } // Catchall if max rating of country is less than 21 // Using 21 instead of 18 to be sure to allow access to all rated content except adult and banned - if (!ratings.Any(x => x.Value >= 21)) + if (!ratings.Any(x => x.RatingScore?.Score >= 21)) { - ratings.Add(new ParentalRating("21", 21)); + ratings.Add(new ParentalRating("21", new(21, null))); } // A lot of countries don't explicitly have a separate rating for adult content - if (ratings.All(x => x.Value != 1000)) + if (ratings.All(x => x.RatingScore?.Score != 1000)) { - ratings.Add(new ParentalRating("XXX", 1000)); + ratings.Add(new ParentalRating("XXX", new(1000, null))); } // A lot of countries don't explicitly have a separate rating for banned content - if (ratings.All(x => x.Value != 1001)) + if (ratings.All(x => x.RatingScore?.Score != 1001)) { - ratings.Add(new ParentalRating("Banned", 1001)); + ratings.Add(new ParentalRating("Banned", new(1001, null))); } - return ratings.OrderBy(r => r.Value); + return [.. ratings.OrderBy(r => r.RatingScore?.Score).ThenBy(r => r.RatingScore?.SubScore)]; } /// /// Gets the parental ratings dictionary. /// /// The optional two letter ISO language string. - /// . - private Dictionary? GetParentalRatingsDictionary(string? countryCode = null) + /// . + private Dictionary? GetParentalRatingsDictionary(string? countryCode = null) { // Fallback to server default if no country code is specified. if (string.IsNullOrEmpty(countryCode)) @@ -268,7 +261,7 @@ namespace Emby.Server.Implementations.Localization } /// - public int? GetRatingLevel(string rating, string? countryCode = null) + public ParentalRatingScore? GetRatingScore(string rating, string? countryCode = null) { ArgumentException.ThrowIfNullOrEmpty(rating); @@ -278,11 +271,11 @@ namespace Emby.Server.Implementations.Localization return null; } - // Convert integers directly + // Convert ints directly // This may override some of the locale specific age ratings (but those always map to the same age) if (int.TryParse(rating, out var ratingAge)) { - return ratingAge; + return new(ratingAge, null); } // Fairly common for some users to have "Rated R" in their rating field @@ -295,9 +288,9 @@ namespace Emby.Server.Implementations.Localization if (!string.IsNullOrEmpty(countryCode)) { var ratingsDictionary = GetParentalRatingsDictionary(countryCode); - if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value)) + if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value)) { - return value.Value; + return value; } } else @@ -305,9 +298,9 @@ namespace Emby.Server.Implementations.Localization // Fall back to server default language for ratings check // If it has no ratings, use the US ratings var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us"); - if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRating? value)) + if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value)) { - return value.Value; + return value; } } @@ -316,7 +309,7 @@ namespace Emby.Server.Implementations.Localization { if (dictionary.TryGetValue(rating, out var value)) { - return value.Value; + return value; } } @@ -326,7 +319,7 @@ namespace Emby.Server.Implementations.Localization var ratingLevelRightPart = rating.AsSpan().RightPart(':'); if (ratingLevelRightPart.Length != 0) { - return GetRatingLevel(ratingLevelRightPart.ToString()); + return GetRatingScore(ratingLevelRightPart.ToString()); } } @@ -342,7 +335,7 @@ namespace Emby.Server.Implementations.Localization if (ratingLevelRightPart.Length != 0) { // Check rating system of culture - return GetRatingLevel(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName); + return GetRatingScore(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName); } } @@ -406,7 +399,7 @@ namespace Emby.Server.Implementations.Localization private async Task CopyInto(IDictionary dictionary, string resourcePath) { - await using var stream = _assembly.GetManifestResourceStream(resourcePath); + using var stream = _assembly.GetManifestResourceStream(resourcePath); // If a Culture doesn't have a translation the stream will be null and it defaults to en-us further up the chain if (stream is null) { @@ -414,12 +407,7 @@ namespace Emby.Server.Implementations.Localization return; } - var dict = await JsonSerializer.DeserializeAsync>(stream, _jsonOptions).ConfigureAwait(false); - if (dict is null) - { - throw new InvalidOperationException($"Resource contains invalid data: '{stream}'"); - } - + var dict = await JsonSerializer.DeserializeAsync>(stream, _jsonOptions).ConfigureAwait(false) ?? throw new InvalidOperationException($"Resource contains invalid data: '{stream}'"); foreach (var key in dict.Keys) { dictionary[key] = dict[key]; diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv b/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv deleted file mode 100644 index 36886ba760..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/0-prefer.csv +++ /dev/null @@ -1,11 +0,0 @@ -E,0 -EC,0 -T,7 -M,18 -AO,18 -UR,18 -RP,18 -X,1000 -XX,1000 -XXX,1000 -XXXX,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/0-prefer.json b/Emby.Server.Implementations/Localization/Ratings/0-prefer.json new file mode 100644 index 0000000000..b390151611 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/0-prefer.json @@ -0,0 +1,34 @@ +{ + "countryCode": "0-prefer", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["E", "EC"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["T"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["M", "AO", "UR", "RP"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X", "XX", "XXX", "XXXX"], + "ratingScore": { + "score": 1000, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv deleted file mode 100644 index 6e12759a46..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/au.csv +++ /dev/null @@ -1,17 +0,0 @@ -Exempt,0 -G,0 -7+,7 -PG,15 -M,15 -MA,15 -MA15+,15 -MA 15+,15 -16+,16 -R,18 -R18+,18 -R 18+,18 -18+,18 -X18+,1000 -X 18+,1000 -X,1000 -RC,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/au.json b/Emby.Server.Implementations/Localization/Ratings/au.json new file mode 100644 index 0000000000..a563df899d --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/au.json @@ -0,0 +1,69 @@ +{ + "countryCode": "au", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["Exempt", "G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["7+"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG"], + "ratingScore": { + "score": 15, + "subScore": 1 + } + }, + { + "ratingStrings": ["M"], + "ratingScore": { + "score": 15, + "subScore": 2 + } + }, + { + "ratingStrings": ["MA", "MA 15+", "MA15+"], + "ratingScore": { + "score": 15, + "subScore": 3 + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18+", "R", "R18+", "R 18+"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["X", "X18", "X 18"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + }, + { + "ratingStrings": ["RC"], + "ratingScore": { + "score": 1001, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/be.csv b/Emby.Server.Implementations/Localization/Ratings/be.csv deleted file mode 100644 index d171a71328..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/be.csv +++ /dev/null @@ -1,11 +0,0 @@ -AL,0 -KT,0 -TOUS,0 -MG6,6 -6,6 -9,9 -KNT,12 -12,12 -14,14 -16,16 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/be.json b/Emby.Server.Implementations/Localization/Ratings/be.json new file mode 100644 index 0000000000..18ea2c2605 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/be.json @@ -0,0 +1,55 @@ +{ + "countryCode": "be", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["AL", "KT", "TOUS"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6", "MG6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["12", "KNT"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/br.csv b/Emby.Server.Implementations/Localization/Ratings/br.csv deleted file mode 100644 index f6053c88c7..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/br.csv +++ /dev/null @@ -1,14 +0,0 @@ -Livre,0 -L,0 -AL,0 -ER,10 -10,10 -A10,10 -12,12 -A12,12 -14,14 -A14,14 -16,16 -A16,16 -18,18 -A18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/br.json b/Emby.Server.Implementations/Localization/Ratings/br.json new file mode 100644 index 0000000000..f455b6643f --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/br.json @@ -0,0 +1,55 @@ +{ + "countryCode": "br", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["L", "AL", "Livre"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["10", "A10", "ER"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["12", "A12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["14", "A14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16", "A16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "A18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.csv b/Emby.Server.Implementations/Localization/Ratings/ca.csv deleted file mode 100644 index 41dbda1349..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ca.csv +++ /dev/null @@ -1,18 +0,0 @@ -E,0 -G,0 -TV-Y,0 -TV-G,0 -TV-Y7,7 -TV-Y7-FV,7 -PG,9 -TV-PG,9 -TV-14,14 -14A,14 -16+,16 -NC-17,17 -R,18 -TV-MA,18 -18A,18 -18+,18 -A,1000 -Prohibited,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.json b/Emby.Server.Implementations/Localization/Ratings/ca.json new file mode 100644 index 0000000000..fa43a8f2b7 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ca.json @@ -0,0 +1,90 @@ +{ + "countryCode": "ca", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["E", "G", "TV-Y", "TV-G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-Y7"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-Y7-FV"], + "ratingScore": { + "score": 7, + "subScore": 1 + } + }, + { + "ratingStrings": ["PG", "TV-PG"], + "ratingScore": { + "score": 9, + "subScore": 0 + } + }, + { + "ratingStrings": ["14A"], + "ratingScore": { + "score": 14, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-14"], + "ratingScore": { + "score": 14, + "subScore": 1 + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["NC-17"], + "ratingScore": { + "score": 17, + "subScore": 0 + } + }, + { + "ratingStrings": ["18A"], + "ratingScore": { + "score": 18, + "subScore": 0 + } + }, + { + "ratingStrings": ["18+", "TV-MA", "R"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["A"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + }, + { + "ratingStrings": ["Prohibited"], + "ratingScore": { + "score": 1001, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/cl.json b/Emby.Server.Implementations/Localization/Ratings/cl.json new file mode 100644 index 0000000000..0866194715 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/cl.json @@ -0,0 +1,41 @@ +{ + "countryCode": "cl", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["TE"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["TE+7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["18", "18V", "18S"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/co.csv b/Emby.Server.Implementations/Localization/Ratings/co.csv deleted file mode 100644 index e1e96c5909..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/co.csv +++ /dev/null @@ -1,7 +0,0 @@ -T,0 -7,7 -12,12 -15,15 -18,18 -X,1000 -Prohibited,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/co.json b/Emby.Server.Implementations/Localization/Ratings/co.json new file mode 100644 index 0000000000..4eff6dcc53 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/co.json @@ -0,0 +1,55 @@ +{ + "countryCode": "co", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["T"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X"], + "ratingScore": { + "score": 1000, + "subScore": null + } + }, + { + "ratingStrings": ["Prohibited"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv deleted file mode 100644 index f6181575e2..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/de.csv +++ /dev/null @@ -1,17 +0,0 @@ -Educational,0 -Infoprogramm,0 -FSK-0,0 -FSK 0,0 -0,0 -FSK-6,6 -FSK 6,6 -6,6 -FSK-12,12 -FSK 12,12 -12,12 -FSK-16,16 -FSK 16,16 -16,16 -FSK-18,18 -FSK 18,18 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/de.json b/Emby.Server.Implementations/Localization/Ratings/de.json new file mode 100644 index 0000000000..30c34b230c --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/de.json @@ -0,0 +1,41 @@ +{ + "countryCode": "de", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0", "FSK 0", "FSK-0", "Educational", "Infoprogramm"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6", "FSK 6", "FSK-6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["12", "FSK 12", "FSK-12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16", "FSK 16", "FSK-16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "FSK 18", "FSK-18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.csv b/Emby.Server.Implementations/Localization/Ratings/dk.csv deleted file mode 100644 index 4ef63b2eac..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/dk.csv +++ /dev/null @@ -1,7 +0,0 @@ -F,0 -A,0 -7,7 -11,11 -12,12 -15,15 -16,16 diff --git a/Emby.Server.Implementations/Localization/Ratings/dk.json b/Emby.Server.Implementations/Localization/Ratings/dk.json new file mode 100644 index 0000000000..9fcd6d44fd --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/dk.json @@ -0,0 +1,48 @@ +{ + "countryCode": "dk", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["F", "A"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["11"], + "ratingScore": { + "score": 11, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv deleted file mode 100644 index ee58660900..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/es.csv +++ /dev/null @@ -1,25 +0,0 @@ -A,0 -A/fig,0 -A/i,0 -A/i/fig,0 -APTA,0 -ERI,0 -TP,0 -0+,0 -6+,6 -7/fig,7 -7/i,7 -7/i/fig,7 -7,7 -9+,9 -10,10 -12,12 -12/fig,12 -13,13 -14,14 -16,16 -16/fig,16 -18,18 -18/fig,18 -X,1000 -Banned,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/es.json b/Emby.Server.Implementations/Localization/Ratings/es.json new file mode 100644 index 0000000000..c19629939d --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/es.json @@ -0,0 +1,90 @@ +{ + "countryCode": "es", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+", "A", "A/i", "A/fig", "A/i/fig", "APTA", "ERI", "TP"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["7", "7/i", "7/fig", "7/i/fig"], + "ratingScore": { + "score": 11, + "subScore": null + } + }, + { + "ratingStrings": ["9+"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["10"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["12", "12/fig"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["13"], + "ratingScore": { + "score": 13, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16", "16/fig"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "18/fig"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X"], + "ratingScore": { + "score": 1000, + "subScore": null + } + }, + { + "ratingStrings": ["Banned"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.csv b/Emby.Server.Implementations/Localization/Ratings/fi.csv deleted file mode 100644 index 7ff92f259b..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/fi.csv +++ /dev/null @@ -1,10 +0,0 @@ -S,0 -T,0 -K7,7 -7,7 -K12,12 -12,12 -K16,16 -16,16 -K18,18 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/fi.json b/Emby.Server.Implementations/Localization/Ratings/fi.json new file mode 100644 index 0000000000..3152317b59 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/fi.json @@ -0,0 +1,41 @@ +{ + "countryCode": "fi", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["S", "T"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7", "K7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["12", "K12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16", "K16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "K18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.csv b/Emby.Server.Implementations/Localization/Ratings/fr.csv deleted file mode 100644 index 139ea376b7..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/fr.csv +++ /dev/null @@ -1,13 +0,0 @@ -Public Averti,0 -Tous Publics,0 -TP,0 -U,0 -0+,0 -6+,6 -9+,9 -10,10 -12,12 -14+,14 -16,16 -18,18 -X,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.json b/Emby.Server.Implementations/Localization/Ratings/fr.json new file mode 100644 index 0000000000..e8bafd6b87 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/fr.json @@ -0,0 +1,69 @@ +{ + "countryCode": "fr", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+", "Public Averti", "Tous Publics", "TP", "U"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["9+"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["10"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["14+"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["X"], + "ratingScore": { + "score": 1000, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.csv b/Emby.Server.Implementations/Localization/Ratings/gb.csv deleted file mode 100644 index 858b9a32dd..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/gb.csv +++ /dev/null @@ -1,23 +0,0 @@ -All,0 -E,0 -G,0 -U,0 -0+,0 -6+,6 -7+,7 -PG,8 -9,9 -12,12 -12+,12 -12A,12 -12PG,12 -Teen,13 -13+,13 -14+,14 -15,15 -16,16 -Caution,18 -18,18 -Mature,1000 -Adult,1000 -R18,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.json b/Emby.Server.Implementations/Localization/Ratings/gb.json new file mode 100644 index 0000000000..7fc88272cf --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/gb.json @@ -0,0 +1,97 @@ +{ + "countryCode": "gb", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["0+", "All", "E", "G", "U"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": 0 + } + }, + { + "ratingStrings": ["7+"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG"], + "ratingScore": { + "score": 8, + "subScore": 0 + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": 0 + } + }, + { + "ratingStrings": ["12A", "12PG"], + "ratingScore": { + "score": 12, + "subScore": 0 + } + }, + { + "ratingStrings": ["12", "12+"], + "ratingScore": { + "score": 12, + "subScore": 1 + } + }, + { + "ratingStrings": ["13+", "Teen"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["14+"], + "ratingScore": { + "score": 14, + "subScore": 0 + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": 3 + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18", "Caution"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["Mature", "Adult", "R18"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.csv b/Emby.Server.Implementations/Localization/Ratings/ie.csv deleted file mode 100644 index d3c634fc93..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ie.csv +++ /dev/null @@ -1,10 +0,0 @@ -G,4 -PG,12 -12,12 -12A,12 -12PG,12 -15,15 -15PG,15 -15A,15 -16,16 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.json b/Emby.Server.Implementations/Localization/Ratings/ie.json new file mode 100644 index 0000000000..f6cc56ed6d --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ie.json @@ -0,0 +1,55 @@ +{ + "countryCode": "ie", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["G"], + "ratingScore": { + "score": 4, + "subScore": 0 + } + }, + { + "ratingStrings": ["12A", "12PG", "PG"], + "ratingScore": { + "score": 12, + "subScore": 0 + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": 1 + } + }, + { + "ratingStrings": ["15A", "15PG"], + "ratingScore": { + "score": 15, + "subScore": 0 + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": 3 + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.csv b/Emby.Server.Implementations/Localization/Ratings/jp.csv deleted file mode 100644 index bfb5fdaae9..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/jp.csv +++ /dev/null @@ -1,11 +0,0 @@ -A,0 -G,0 -B,12 -PG12,12 -C,15 -15+,15 -R15+,15 -16+,16 -D,17 -Z,18 -18+,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/jp.json b/Emby.Server.Implementations/Localization/Ratings/jp.json new file mode 100644 index 0000000000..efff9e92ce --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/jp.json @@ -0,0 +1,62 @@ +{ + "countryCode": "jp", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["A", "G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG12"], + "ratingScore": { + "score": 12, + "subScore": 0 + } + }, + { + "ratingStrings": ["B"], + "ratingScore": { + "score": 12, + "subScore": 1 + } + }, + { + "ratingStrings": ["15A", "15PG"], + "ratingScore": { + "score": 15, + "subScore": 0 + } + }, + { + "ratingStrings": ["C", "15+", "R15+"], + "ratingScore": { + "score": 15, + "subScore": 1 + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["D"], + "ratingScore": { + "score": 17, + "subScore": null + } + }, + { + "ratingStrings": ["18+", "Z"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.csv b/Emby.Server.Implementations/Localization/Ratings/kz.csv deleted file mode 100644 index e26b32b67e..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/kz.csv +++ /dev/null @@ -1,6 +0,0 @@ -K,0 -БА,12 -Б14,14 -E16,16 -E18,18 -HA,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/kz.json b/Emby.Server.Implementations/Localization/Ratings/kz.json new file mode 100644 index 0000000000..0f8f0c68e5 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/kz.json @@ -0,0 +1,41 @@ +{ + "countryCode": "kz", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["K"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["БА"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["Б14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["E16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["E18", "HA"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.csv b/Emby.Server.Implementations/Localization/Ratings/mx.csv deleted file mode 100644 index 305912f239..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/mx.csv +++ /dev/null @@ -1,6 +0,0 @@ -A,0 -AA,0 -B,12 -B-15,15 -C,18 -D,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/mx.json b/Emby.Server.Implementations/Localization/Ratings/mx.json new file mode 100644 index 0000000000..9dc3b89bd6 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/mx.json @@ -0,0 +1,41 @@ +{ + "countryCode": "mx", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["A", "AA"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["B"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["B-15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["C"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["D"], + "ratingScore": { + "score": 1000, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.csv b/Emby.Server.Implementations/Localization/Ratings/nl.csv deleted file mode 100644 index 44f372b2d6..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/nl.csv +++ /dev/null @@ -1,8 +0,0 @@ -AL,0 -MG6,6 -6,6 -9,9 -12,12 -14,14 -16,16 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/nl.json b/Emby.Server.Implementations/Localization/Ratings/nl.json new file mode 100644 index 0000000000..2e43eb83ab --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/nl.json @@ -0,0 +1,55 @@ +{ + "countryCode": "nl", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["AL"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6", "MG6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/no.csv b/Emby.Server.Implementations/Localization/Ratings/no.csv deleted file mode 100644 index 6856a2dbbb..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/no.csv +++ /dev/null @@ -1,10 +0,0 @@ -A,0 -6,6 -7,7 -9,9 -11,11 -12,12 -15,15 -18,18 -C,18 -Not approved,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/no.json b/Emby.Server.Implementations/Localization/Ratings/no.json new file mode 100644 index 0000000000..a5e9523163 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/no.json @@ -0,0 +1,69 @@ +{ + "countryCode": "no", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["A"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["11"], + "ratingScore": { + "score": 11, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["Not approved"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.csv b/Emby.Server.Implementations/Localization/Ratings/nz.csv deleted file mode 100644 index 633da78fe1..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/nz.csv +++ /dev/null @@ -1,16 +0,0 @@ -Exempt,0 -G,0 -GY,13 -PG,13 -R13,13 -RP13,13 -R15,15 -M,16 -R16,16 -RP16,16 -GA,18 -R18,18 -RP18,18 -MA,1000 -R,1001 -Objectionable,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.json b/Emby.Server.Implementations/Localization/Ratings/nz.json new file mode 100644 index 0000000000..3c1332271e --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/nz.json @@ -0,0 +1,69 @@ +{ + "countryCode": "nz", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["Exempt", "G"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["RP13", "PG"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["GY", "R13"], + "ratingScore": { + "score": 13, + "subScore": 1 + } + }, + { + "ratingStrings": ["RP16", "M"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["R16"], + "ratingScore": { + "score": 16, + "subScore": 1 + } + }, + { + "ratingStrings": ["RP18"], + "ratingScore": { + "score": 18, + "subScore": 0 + } + }, + { + "ratingStrings": ["R18", "GA"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["MA"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + }, + { + "ratingStrings": ["Objectionable", "R"], + "ratingScore": { + "score": 1001, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.csv b/Emby.Server.Implementations/Localization/Ratings/ro.csv deleted file mode 100644 index 44c23e2486..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ro.csv +++ /dev/null @@ -1,6 +0,0 @@ -AG,0 -AP-12,12 -N-15,15 -IM-18,18 -IM-18-XXX,1000 -IC,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ro.json b/Emby.Server.Implementations/Localization/Ratings/ro.json new file mode 100644 index 0000000000..9cf735a54c --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ro.json @@ -0,0 +1,48 @@ +{ + "countryCode": "ro", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["AG"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["AP-12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["N-15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["IM-18"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["IM-18-XXX"], + "ratingScore": { + "score": 1000, + "subScore": null + } + }, + { + "ratingStrings": ["IC"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.csv b/Emby.Server.Implementations/Localization/Ratings/ru.csv deleted file mode 100644 index 8b264070ba..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/ru.csv +++ /dev/null @@ -1,6 +0,0 @@ -0+,0 -6+,6 -12+,12 -16+,16 -18+,18 -Refused classification,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/ru.json b/Emby.Server.Implementations/Localization/Ratings/ru.json new file mode 100644 index 0000000000..d1b8b13aa0 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/ru.json @@ -0,0 +1,48 @@ +{ + "countryCode": "ru", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": null + } + }, + { + "ratingStrings": ["12+"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16+"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18+"], + "ratingScore": { + "score": 18, + "subScore": null + } + }, + { + "ratingStrings": ["Refused classification"], + "ratingScore": { + "score": 1001, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/se.csv b/Emby.Server.Implementations/Localization/Ratings/se.csv deleted file mode 100644 index e129c35617..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/se.csv +++ /dev/null @@ -1,10 +0,0 @@ -Alla,0 -Barntillåten,0 -Btl,0 -0+,0 -7,7 -9+,9 -10+,10 -11,11 -14,14 -15,15 diff --git a/Emby.Server.Implementations/Localization/Ratings/se.json b/Emby.Server.Implementations/Localization/Ratings/se.json new file mode 100644 index 0000000000..70084995d1 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/se.json @@ -0,0 +1,55 @@ +{ + "countryCode": "se", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["0+", "Alla", "Barntillåten", "Btl"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["9+"], + "ratingScore": { + "score": 9, + "subScore": null + } + }, + { + "ratingStrings": ["10+"], + "ratingScore": { + "score": 10, + "subScore": null + } + }, + { + "ratingStrings": ["11"], + "ratingScore": { + "score": 11, + "subScore": null + } + }, + { + "ratingStrings": ["14"], + "ratingScore": { + "score": 14, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.csv b/Emby.Server.Implementations/Localization/Ratings/sk.csv deleted file mode 100644 index dbafd8efa3..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/sk.csv +++ /dev/null @@ -1,6 +0,0 @@ -NR,0 -U,0 -7,7 -12,12 -15,15 -18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.json b/Emby.Server.Implementations/Localization/Ratings/sk.json new file mode 100644 index 0000000000..5ec6111ecd --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/sk.json @@ -0,0 +1,41 @@ +{ + "countryCode": "sk", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["U", "NR"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["12"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": null + } + }, + { + "ratingStrings": ["18"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.csv b/Emby.Server.Implementations/Localization/Ratings/uk.csv deleted file mode 100644 index 75b1c20589..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/uk.csv +++ /dev/null @@ -1,22 +0,0 @@ -All,0 -E,0 -G,0 -U,0 -0+,0 -6+,6 -7+,7 -PG,8 -9+,9 -12,12 -12+,12 -12A,12 -Teen,13 -13+,13 -14+,14 -15,15 -16,16 -Caution,18 -18,18 -Mature,1000 -Adult,1000 -R18,1000 diff --git a/Emby.Server.Implementations/Localization/Ratings/uk.json b/Emby.Server.Implementations/Localization/Ratings/uk.json new file mode 100644 index 0000000000..7fc88272cf --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/uk.json @@ -0,0 +1,97 @@ +{ + "countryCode": "gb", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["0+", "All", "E", "G", "U"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["6+"], + "ratingScore": { + "score": 6, + "subScore": 0 + } + }, + { + "ratingStrings": ["7+"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["PG"], + "ratingScore": { + "score": 8, + "subScore": 0 + } + }, + { + "ratingStrings": ["9"], + "ratingScore": { + "score": 9, + "subScore": 0 + } + }, + { + "ratingStrings": ["12A", "12PG"], + "ratingScore": { + "score": 12, + "subScore": 0 + } + }, + { + "ratingStrings": ["12", "12+"], + "ratingScore": { + "score": 12, + "subScore": 1 + } + }, + { + "ratingStrings": ["13+", "Teen"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["14+"], + "ratingScore": { + "score": 14, + "subScore": 0 + } + }, + { + "ratingStrings": ["15"], + "ratingScore": { + "score": 15, + "subScore": 3 + } + }, + { + "ratingStrings": ["16"], + "ratingScore": { + "score": 16, + "subScore": 0 + } + }, + { + "ratingStrings": ["18", "Caution"], + "ratingScore": { + "score": 18, + "subScore": 1 + } + }, + { + "ratingStrings": ["Mature", "Adult", "R18"], + "ratingScore": { + "score": 1000, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Localization/Ratings/us.csv b/Emby.Server.Implementations/Localization/Ratings/us.csv deleted file mode 100644 index 9aa5c00eb3..0000000000 --- a/Emby.Server.Implementations/Localization/Ratings/us.csv +++ /dev/null @@ -1,52 +0,0 @@ -Approved,0 -G,0 -TV-G,0 -TV-Y,0 -TV-Y7,7 -TV-Y7-FV,7 -PG,10 -TV-PG,10 -TV-PG-D,10 -TV-PG-L,10 -TV-PG-S,10 -TV-PG-V,10 -TV-PG-DL,10 -TV-PG-DS,10 -TV-PG-DV,10 -TV-PG-LS,10 -TV-PG-LV,10 -TV-PG-SV,10 -TV-PG-DLS,10 -TV-PG-DLV,10 -TV-PG-DSV,10 -TV-PG-LSV,10 -TV-PG-DLSV,10 -PG-13,13 -TV-14,14 -TV-14-D,14 -TV-14-L,14 -TV-14-S,14 -TV-14-V,14 -TV-14-DL,14 -TV-14-DS,14 -TV-14-DV,14 -TV-14-LS,14 -TV-14-LV,14 -TV-14-SV,14 -TV-14-DLS,14 -TV-14-DLV,14 -TV-14-DSV,14 -TV-14-LSV,14 -TV-14-DLSV,14 -NC-17,17 -R,17 -TV-MA,17 -TV-MA-L,17 -TV-MA-S,17 -TV-MA-V,17 -TV-MA-LS,17 -TV-MA-LV,17 -TV-MA-SV,17 -TV-MA-LSV,17 -TV-X,18 -TV-AO,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/us.json b/Emby.Server.Implementations/Localization/Ratings/us.json new file mode 100644 index 0000000000..08a6373129 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/us.json @@ -0,0 +1,83 @@ +{ + "countryCode": "us", + "supportsSubScores": true, + "ratings": [ + { + "ratingStrings": ["Approved", "G", "TV-G", "TV-Y"], + "ratingScore": { + "score": 0, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-Y7"], + "ratingScore": { + "score": 7, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-Y7-FV"], + "ratingScore": { + "score": 7, + "subScore": 1 + } + }, + { + "ratingStrings": ["PG", "TV-PG"], + "ratingScore": { + "score": 10, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-PG-D", "TV-PG-L", "TV-PG-S", "TV-PG-V", "TV-PG-DL", "TV-PG-DS", "TV-PG-DV", "TV-PG-LS", "TV-PG-LV", "TV-PG-SV", "TV-PG-DLS", "TV-PG-DLV", "TV-PG-DSV", "TV-PG-LSV", "TV-PG-DLSV"], + "ratingScore": { + "score": 10, + "subScore": 1 + } + }, + { + "ratingStrings": ["PG-13"], + "ratingScore": { + "score": 13, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-14"], + "ratingScore": { + "score": 14, + "subScore": 0 + } + }, + { + "ratingStrings": ["TV-14-D", "TV-14-L", "TV-14-S", "TV-14-V", "TV-14-DL", "TV-14-DS", "TV-14-DV", "TV-14-LS", "TV-14-LV", "TV-14-SV", "TV-14-DLS", "TV-14-DLV", "TV-14-DSV", "TV-14-LSV", "TV-14-DLSV"], + "ratingScore": { + "score": 14, + "subScore": 1 + } + }, + { + "ratingStrings": ["R"], + "ratingScore": { + "score": 17, + "subScore": 0 + } + }, + { + "ratingStrings": ["NC-17", "TV-MA", "TV-MA-L", "TV-MA-S", "TV-MA-V", "TV-MA-LS", "TV-MA-LV", "TV-MA-SV", "TV-MA-LSV"], + "ratingScore": { + "score": 17, + "subScore": 1 + } + }, + { + "ratingStrings": ["TV-X", "TV-AO"], + "ratingScore": { + "score": 18, + "subScore": 0 + } + } + ] +} diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs index b4ee2c7234..789af01cc3 100644 --- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs @@ -1,45 +1,54 @@ -#pragma warning disable CS1591 - using System; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Querying; -namespace Emby.Server.Implementations.Sorting +namespace Emby.Server.Implementations.Sorting; + +/// +/// Class providing comparison for official ratings. +/// +public class OfficialRatingComparer : IBaseItemComparer { - public class OfficialRatingComparer : IBaseItemComparer + private readonly ILocalizationManager _localizationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public OfficialRatingComparer(ILocalizationManager localizationManager) { - private readonly ILocalizationManager _localization; + _localizationManager = localizationManager; + } - public OfficialRatingComparer(ILocalizationManager localization) + /// + /// Gets the name. + /// + /// The name. + public ItemSortBy Type => ItemSortBy.OfficialRating; + + /// + /// Compares the specified x. + /// + /// The x. + /// The y. + /// System.Int32. + public int Compare(BaseItem? x, BaseItem? y) + { + ArgumentNullException.ThrowIfNull(x); + ArgumentNullException.ThrowIfNull(y); + var zeroRating = new ParentalRatingScore(0, 0); + + var ratingX = string.IsNullOrEmpty(x.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(x.OfficialRating) ?? zeroRating; + var ratingY = string.IsNullOrEmpty(y.OfficialRating) ? zeroRating : _localizationManager.GetRatingScore(y.OfficialRating) ?? zeroRating; + var scoreCompare = ratingX.Score.CompareTo(ratingY.Score); + if (scoreCompare is 0) { - _localization = localization; + return (ratingX.SubScore ?? 0).CompareTo(ratingY.SubScore ?? 0); } - /// - /// Gets the name. - /// - /// The name. - public ItemSortBy Type => ItemSortBy.OfficialRating; - - /// - /// Compares the specified x. - /// - /// The x. - /// The y. - /// System.Int32. - public int Compare(BaseItem? x, BaseItem? y) - { - ArgumentNullException.ThrowIfNull(x); - - ArgumentNullException.ThrowIfNull(y); - - var levelX = string.IsNullOrEmpty(x.OfficialRating) ? 0 : _localization.GetRatingLevel(x.OfficialRating) ?? 0; - var levelY = string.IsNullOrEmpty(y.OfficialRating) ? 0 : _localization.GetRatingLevel(y.OfficialRating) ?? 0; - - return levelX.CompareTo(levelY); - } + return scoreCompare; } } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 803c2f1f78..a491283363 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -448,13 +448,13 @@ public class ItemsController : BaseJellyfinApiController // Min official rating if (!string.IsNullOrWhiteSpace(minOfficialRating)) { - query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating); + query.MinParentalRating = _localization.GetRatingScore(minOfficialRating); } // Max official rating if (!string.IsNullOrWhiteSpace(maxOfficialRating)) { - query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating); + query.MaxParentalRating = _localization.GetRatingScore(maxOfficialRating); } // Artists diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs index f65d95c411..bbce5a9e13 100644 --- a/Jellyfin.Api/Controllers/LocalizationController.cs +++ b/Jellyfin.Api/Controllers/LocalizationController.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Jellyfin.Api.Constants; using MediaBrowser.Common.Api; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -45,7 +44,7 @@ public class LocalizationController : BaseJellyfinApiController /// An containing the list of countries. [HttpGet("Countries")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetCountries() + public ActionResult> GetCountries() { return Ok(_localization.GetCountries()); } @@ -57,7 +56,7 @@ public class LocalizationController : BaseJellyfinApiController /// An containing the list of parental ratings. [HttpGet("ParentalRatings")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetParentalRatings() + public ActionResult> GetParentalRatings() { return Ok(_localization.GetParentalRatings()); } diff --git a/Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs new file mode 100644 index 0000000000..d70ac672f2 --- /dev/null +++ b/Jellyfin.Server.Implementations/Extensions/ExpressionExtensions.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace Jellyfin.Server.Implementations.Extensions; + +/// +/// Provides extension methods. +/// +public static class ExpressionExtensions +{ + /// + /// Combines two predicates into a single predicate using a logical OR operation. + /// + /// The predicate parameter type. + /// The first predicate expression to combine. + /// The second predicate expression to combine. + /// A new expression representing the OR combination of the input predicates. + public static Expression> Or(this Expression> firstPredicate, Expression> secondPredicate) + { + ArgumentNullException.ThrowIfNull(firstPredicate); + ArgumentNullException.ThrowIfNull(secondPredicate); + + var invokedExpression = Expression.Invoke(secondPredicate, firstPredicate.Parameters); + return Expression.Lambda>(Expression.OrElse(firstPredicate.Body, invokedExpression), firstPredicate.Parameters); + } + + /// + /// Combines multiple predicates into a single predicate using a logical OR operation. + /// + /// The predicate parameter type. + /// A collection of predicate expressions to combine. + /// A new expression representing the OR combination of all input predicates. + public static Expression> Or(this IEnumerable>> predicates) + { + ArgumentNullException.ThrowIfNull(predicates); + + return predicates.Aggregate((aggregatePredicate, nextPredicate) => aggregatePredicate.Or(nextPredicate)); + } + + /// + /// Combines two predicates into a single predicate using a logical AND operation. + /// + /// The predicate parameter type. + /// The first predicate expression to combine. + /// The second predicate expression to combine. + /// A new expression representing the AND combination of the input predicates. + public static Expression> And(this Expression> firstPredicate, Expression> secondPredicate) + { + ArgumentNullException.ThrowIfNull(firstPredicate); + ArgumentNullException.ThrowIfNull(secondPredicate); + + var invokedExpression = Expression.Invoke(secondPredicate, firstPredicate.Parameters); + return Expression.Lambda>(Expression.AndAlso(firstPredicate.Body, invokedExpression), firstPredicate.Parameters); + } + + /// + /// Combines multiple predicates into a single predicate using a logical AND operation. + /// + /// The predicate parameter type. + /// A collection of predicate expressions to combine. + /// A new expression representing the AND combination of all input predicates. + public static Expression> And(this IEnumerable>> predicates) + { + ArgumentNullException.ThrowIfNull(predicates); + + return predicates.Aggregate((aggregatePredicate, nextPredicate) => aggregatePredicate.And(nextPredicate)); + } +} diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index b0a36b3ae6..08c024f437 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -9,6 +9,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Text; using System.Text.Json; @@ -19,6 +20,7 @@ using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; +using Jellyfin.Server.Implementations.Extensions; using MediaBrowser.Common; using MediaBrowser.Controller; using MediaBrowser.Controller.Channels; @@ -781,6 +783,7 @@ public sealed class BaseItemRepository entity.PreferredMetadataCountryCode = dto.PreferredMetadataCountryCode; entity.IsInMixedFolder = dto.IsInMixedFolder; entity.InheritedParentalRatingValue = dto.InheritedParentalRatingValue; + entity.InheritedParentalRatingSubValue = dto.InheritedParentalRatingSubValue; entity.CriticRating = dto.CriticRating; entity.PresentationUniqueKey = dto.PresentationUniqueKey; entity.OriginalTitle = dto.OriginalTitle; @@ -1796,62 +1799,74 @@ public sealed class BaseItemRepository .Where(e => filter.OfficialRatings.Contains(e.OfficialRating)); } + Expression>? minParentalRatingFilter = null; + if (filter.MinParentalRating != null) + { + var min = filter.MinParentalRating; + minParentalRatingFilter = e => e.InheritedParentalRatingValue >= min.Score || e.InheritedParentalRatingValue == null; + if (min.SubScore != null) + { + minParentalRatingFilter = minParentalRatingFilter.And(e => e.InheritedParentalRatingValue >= min.SubScore || e.InheritedParentalRatingValue == null); + } + } + + Expression>? maxParentalRatingFilter = null; + if (filter.MaxParentalRating != null) + { + var max = filter.MaxParentalRating; + maxParentalRatingFilter = e => e.InheritedParentalRatingValue <= max.Score || e.InheritedParentalRatingValue == null; + if (max.SubScore != null) + { + maxParentalRatingFilter = maxParentalRatingFilter.And(e => e.InheritedParentalRatingValue <= max.SubScore || e.InheritedParentalRatingValue == null); + } + } + if (filter.HasParentalRating ?? false) { - if (filter.MinParentalRating.HasValue) + if (minParentalRatingFilter != null) { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue >= filter.MinParentalRating.Value); + baseQuery = baseQuery.Where(minParentalRatingFilter); } - if (filter.MaxParentalRating.HasValue) + if (maxParentalRatingFilter != null) { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue < filter.MaxParentalRating.Value); + baseQuery = baseQuery.Where(maxParentalRatingFilter); } } else if (filter.BlockUnratedItems.Length > 0) { - var unratedItems = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray(); - if (filter.MinParentalRating.HasValue) + var unratedItemTypes = filter.BlockUnratedItems.Select(f => f.ToString()).ToArray(); + Expression> unratedItemFilter = e => e.InheritedParentalRatingValue != null || !unratedItemTypes.Contains(e.UnratedType); + + if (minParentalRatingFilter != null && maxParentalRatingFilter != null) { - if (filter.MaxParentalRating.HasValue) - { - baseQuery = baseQuery - .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType)) - || (e.InheritedParentalRatingValue >= filter.MinParentalRating && e.InheritedParentalRatingValue <= filter.MaxParentalRating)); - } - else - { - baseQuery = baseQuery - .Where(e => (e.InheritedParentalRatingValue == null && !unratedItems.Contains(e.UnratedType)) - || e.InheritedParentalRatingValue >= filter.MinParentalRating); - } + baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter.And(maxParentalRatingFilter))); + } + else if (minParentalRatingFilter != null) + { + baseQuery = baseQuery.Where(unratedItemFilter.And(minParentalRatingFilter)); + } + else if (maxParentalRatingFilter != null) + { + baseQuery = baseQuery.Where(unratedItemFilter.And(maxParentalRatingFilter)); } else { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && !unratedItems.Contains(e.UnratedType)); + baseQuery = baseQuery.Where(unratedItemFilter); } } - else if (filter.MinParentalRating.HasValue) + else if (minParentalRatingFilter != null || maxParentalRatingFilter != null) { - if (filter.MaxParentalRating.HasValue) + if (minParentalRatingFilter != null) { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value && e.InheritedParentalRatingValue <= filter.MaxParentalRating.Value); + baseQuery = baseQuery.Where(minParentalRatingFilter); } - else + + if (maxParentalRatingFilter != null) { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MinParentalRating.Value); + baseQuery = baseQuery.Where(maxParentalRatingFilter); } } - else if (filter.MaxParentalRating.HasValue) - { - baseQuery = baseQuery - .Where(e => e.InheritedParentalRatingValue != null && e.InheritedParentalRatingValue >= filter.MaxParentalRating.Value); - } else if (!filter.HasParentalRating ?? false) { baseQuery = baseQuery diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 3c39e5503b..3dfb14d716 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -342,7 +342,8 @@ namespace Jellyfin.Server.Implementations.Users }, Policy = new UserPolicy { - MaxParentalRating = user.MaxParentalAgeRating, + MaxParentalRating = user.MaxParentalRatingScore, + MaxParentalSubRating = user.MaxParentalRatingSubScore, EnableUserPreferenceAccess = user.EnableUserPreferenceAccess, RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0, AuthenticationProviderId = user.AuthenticationProviderId, @@ -668,7 +669,8 @@ namespace Jellyfin.Server.Implementations.Users _ => policy.LoginAttemptsBeforeLockout }; - user.MaxParentalAgeRating = policy.MaxParentalRating; + user.MaxParentalRatingScore = policy.MaxParentalRating; + user.MaxParentalRatingSubScore = policy.MaxParentalSubRating; user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess; user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit; user.AuthenticationProviderId = policy.AuthenticationProviderId; diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 49568b087a..68a3491b57 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -49,12 +49,12 @@ namespace Jellyfin.Server.Migrations typeof(Routines.RemoveDownloadImagesInAdvance), typeof(Routines.MigrateAuthenticationDb), typeof(Routines.FixPlaylistOwner), - typeof(Routines.MigrateRatingLevels), typeof(Routines.AddDefaultCastReceivers), typeof(Routines.UpdateDefaultPluginRepository), typeof(Routines.FixAudioData), typeof(Routines.RemoveDuplicatePlaylistChildren), typeof(Routines.MigrateLibraryDb), + typeof(Routines.MigrateRatingLevels), typeof(Routines.MoveTrickplayFiles), }; diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index 9c2184029c..c38beb7232 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -1,36 +1,33 @@ using System; -using System.Globalization; -using System.IO; -using Emby.Server.Implementations.Data; -using MediaBrowser.Controller; +using System.Linq; +using Jellyfin.Database.Implementations; using MediaBrowser.Model.Globalization; -using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Routines { /// - /// Migrate rating levels to new rating level system. + /// Migrate rating levels. /// - internal class MigrateRatingLevels : IMigrationRoutine + internal class MigrateRatingLevels : IDatabaseMigrationRoutine { - private const string DbFilename = "library.db"; private readonly ILogger _logger; - private readonly IServerApplicationPaths _applicationPaths; + private readonly IDbContextFactory _provider; private readonly ILocalizationManager _localizationManager; public MigrateRatingLevels( - IServerApplicationPaths applicationPaths, + IDbContextFactory provider, ILoggerFactory loggerFactory, ILocalizationManager localizationManager) { - _applicationPaths = applicationPaths; + _provider = provider; _localizationManager = localizationManager; _logger = loggerFactory.CreateLogger(); } /// - public Guid Id => Guid.Parse("{73DAB92A-178B-48CD-B05B-FE18733ACDC8}"); + public Guid Id => Guid.Parse("{98724538-EB11-40E3-931A-252C55BDDE7A}"); /// public string Name => "MigrateRatingLevels"; @@ -41,54 +38,37 @@ namespace Jellyfin.Server.Migrations.Routines /// public void Perform() { - var dbPath = Path.Combine(_applicationPaths.DataPath, DbFilename); - - // Back up the database before modifying any entries - for (int i = 1; ; i++) - { - var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i); - if (!File.Exists(bakPath)) - { - try - { - File.Copy(dbPath, bakPath); - _logger.LogInformation("Library database backed up to {BackupPath}", bakPath); - break; - } - catch (Exception ex) - { - _logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath); - throw; - } - } - } - - // Migrate parental rating strings to new levels _logger.LogInformation("Recalculating parental rating levels based on rating string."); - using var connection = new SqliteConnection($"Filename={dbPath}"); - connection.Open(); - using (var transaction = connection.BeginTransaction()) + using var context = _provider.CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + var ratings = context.BaseItems.AsNoTracking().Select(e => e.OfficialRating).Distinct(); + foreach (var rating in ratings) { - var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems"); - foreach (var entry in queryResult) + if (string.IsNullOrEmpty(rating)) { - if (!entry.TryGetString(0, out var ratingString) || string.IsNullOrEmpty(ratingString)) - { - connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';"); - } - else - { - var ratingValue = _localizationManager.GetRatingLevel(ratingString)?.ToString(CultureInfo.InvariantCulture) ?? "NULL"; - - using var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;"); - statement.TryBind("@Value", ratingValue); - statement.TryBind("@Rating", ratingString); - statement.ExecuteNonQuery(); - } + int? value = null; + context.BaseItems + .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty) + .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, value)); + context.BaseItems + .Where(e => e.OfficialRating == null || e.OfficialRating == string.Empty) + .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, value)); + } + else + { + var ratingValue = _localizationManager.GetRatingScore(rating); + var score = ratingValue?.Score; + var subScore = ratingValue?.SubScore; + context.BaseItems + .Where(e => e.OfficialRating == rating) + .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingValue, score)); + context.BaseItems + .Where(e => e.OfficialRating == rating) + .ExecuteUpdate(f => f.SetProperty(e => e.InheritedParentalRatingSubValue, subScore)); } - - transaction.Commit(); } + + transaction.Commit(); } } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index c40560660d..1b5fab7a89 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -112,7 +112,8 @@ namespace Jellyfin.Server.Migrations.Routines { Id = entry.GetGuid(1), InternalId = entry.GetInt64(0), - MaxParentalAgeRating = policy.MaxParentalRating, + MaxParentalRatingScore = policy.MaxParentalRating, + MaxParentalRatingSubScore = null, EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess, RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit, InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount, diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 53c832ff37..d484266726 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -581,6 +581,9 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public int? InheritedParentalRatingValue { get; set; } + [JsonIgnore] + public int? InheritedParentalRatingSubValue { get; set; } + /// /// Gets or sets the critic rating. /// @@ -1540,7 +1543,8 @@ namespace MediaBrowser.Controller.Entities return false; } - var maxAllowedRating = user.MaxParentalAgeRating; + var maxAllowedRating = user.MaxParentalRatingScore; + var maxAllowedSubRating = user.MaxParentalRatingSubScore; var rating = CustomRatingForComparison; if (string.IsNullOrEmpty(rating)) @@ -1554,10 +1558,10 @@ namespace MediaBrowser.Controller.Entities return !GetBlockUnratedValue(user); } - var value = LocalizationManager.GetRatingLevel(rating); + var ratingScore = LocalizationManager.GetRatingScore(rating); // Could not determine rating level - if (!value.HasValue) + if (ratingScore is null) { var isAllowed = !GetBlockUnratedValue(user); @@ -1569,10 +1573,15 @@ namespace MediaBrowser.Controller.Entities return isAllowed; } - return !maxAllowedRating.HasValue || value.Value <= maxAllowedRating.Value; + if (maxAllowedSubRating is not null) + { + return (ratingScore.SubScore ?? 0) <= maxAllowedSubRating && ratingScore.Score <= maxAllowedRating.Value; + } + + return !maxAllowedRating.HasValue || ratingScore.Score <= maxAllowedRating.Value; } - public int? GetInheritedParentalRatingValue() + public ParentalRatingScore GetParentalRatingScore() { var rating = CustomRatingForComparison; @@ -1586,7 +1595,7 @@ namespace MediaBrowser.Controller.Entities return null; } - return LocalizationManager.GetRatingLevel(rating); + return LocalizationManager.GetRatingScore(rating); } public List GetInheritedTags() @@ -2518,11 +2527,29 @@ namespace MediaBrowser.Controller.Entities var item = this; - var inheritedParentalRatingValue = item.GetInheritedParentalRatingValue() ?? null; - if (inheritedParentalRatingValue != item.InheritedParentalRatingValue) + var rating = item.GetParentalRatingScore(); + if (rating is not null) { - item.InheritedParentalRatingValue = inheritedParentalRatingValue; - updateType |= ItemUpdateType.MetadataImport; + if (rating.Score != item.InheritedParentalRatingValue) + { + item.InheritedParentalRatingValue = rating.Score; + updateType |= ItemUpdateType.MetadataImport; + } + + if (rating.SubScore != item.InheritedParentalRatingSubValue) + { + item.InheritedParentalRatingSubValue = rating.SubScore; + updateType |= ItemUpdateType.MetadataImport; + } + } + else + { + if (item.InheritedParentalRatingValue is not null) + { + item.InheritedParentalRatingValue = null; + item.InheritedParentalRatingSubValue = null; + updateType |= ItemUpdateType.MetadataImport; + } } return updateType; @@ -2542,8 +2569,9 @@ namespace MediaBrowser.Controller.Entities .Select(i => i.OfficialRating) .Where(i => !string.IsNullOrEmpty(i)) .Distinct(StringComparer.OrdinalIgnoreCase) - .Select(rating => (rating, LocalizationManager.GetRatingLevel(rating))) - .OrderBy(i => i.Item2 ?? 1000) + .Select(rating => (rating, LocalizationManager.GetRatingScore(rating))) + .OrderBy(i => i.Item2 is null ? 1001 : i.Item2.Score) + .ThenBy(i => i.Item2 is null ? 1001 : i.Item2.SubScore) .Select(i => i.rating); OfficialRating = ratings.FirstOrDefault() ?? currentOfficialRating; diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 5ce5fd4fa9..9a83dba458 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -232,9 +232,9 @@ namespace MediaBrowser.Controller.Entities public int? IndexNumber { get; set; } - public int? MinParentalRating { get; set; } + public ParentalRatingScore? MinParentalRating { get; set; } - public int? MaxParentalRating { get; set; } + public ParentalRatingScore? MaxParentalRating { get; set; } public bool? HasDeadParentId { get; set; } @@ -360,16 +360,17 @@ namespace MediaBrowser.Controller.Entities public void SetUser(User user) { - MaxParentalRating = user.MaxParentalAgeRating; - - if (MaxParentalRating.HasValue) + var maxRating = user.MaxParentalRatingScore; + if (maxRating.HasValue) { - string other = UnratedItem.Other.ToString(); - BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems) - .Where(i => i != other) - .Select(e => Enum.Parse(e, true)).ToArray(); + MaxParentalRating = new(maxRating.Value, user.MaxParentalRatingSubScore); } + var other = UnratedItem.Other.ToString(); + BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems) + .Where(i => i != other) + .Select(e => Enum.Parse(e, true)).ToArray(); + ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags); IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags); diff --git a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs index a3035bf612..2f3a5d1179 100644 --- a/MediaBrowser.Model/Dto/MetadataEditorInfo.cs +++ b/MediaBrowser.Model/Dto/MetadataEditorInfo.cs @@ -1,35 +1,55 @@ -#pragma warning disable CS1591 - -using System; using System.Collections.Generic; using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Providers; -namespace MediaBrowser.Model.Dto +namespace MediaBrowser.Model.Dto; + +/// +/// A class representing metadata editor information. +/// +public class MetadataEditorInfo { - public class MetadataEditorInfo + /// + /// Initializes a new instance of the class. + /// + public MetadataEditorInfo() { - public MetadataEditorInfo() - { - ParentalRatingOptions = Array.Empty(); - Countries = Array.Empty(); - Cultures = Array.Empty(); - ExternalIdInfos = Array.Empty(); - ContentTypeOptions = Array.Empty(); - } - - public IReadOnlyList ParentalRatingOptions { get; set; } - - public IReadOnlyList Countries { get; set; } - - public IReadOnlyList Cultures { get; set; } - - public IReadOnlyList ExternalIdInfos { get; set; } - - public CollectionType? ContentType { get; set; } - - public IReadOnlyList ContentTypeOptions { get; set; } + ParentalRatingOptions = []; + Countries = []; + Cultures = []; + ExternalIdInfos = []; + ContentTypeOptions = []; } + + /// + /// Gets or sets the parental rating options. + /// + public IReadOnlyList ParentalRatingOptions { get; set; } + + /// + /// Gets or sets the countries. + /// + public IReadOnlyList Countries { get; set; } + + /// + /// Gets or sets the cultures. + /// + public IReadOnlyList Cultures { get; set; } + + /// + /// Gets or sets the external id infos. + /// + public IReadOnlyList ExternalIdInfos { get; set; } + + /// + /// Gets or sets the content type. + /// + public CollectionType? ContentType { get; set; } + + /// + /// Gets or sets the content type options. + /// + public IReadOnlyList ContentTypeOptions { get; set; } } diff --git a/MediaBrowser.Model/Entities/ParentalRating.cs b/MediaBrowser.Model/Entities/ParentalRating.cs index c92640818c..4f11989021 100644 --- a/MediaBrowser.Model/Entities/ParentalRating.cs +++ b/MediaBrowser.Model/Entities/ParentalRating.cs @@ -1,33 +1,40 @@ -#nullable disable -#pragma warning disable CS1591 +namespace MediaBrowser.Model.Entities; -namespace MediaBrowser.Model.Entities +/// +/// Class ParentalRating. +/// +public class ParentalRating { /// - /// Class ParentalRating. + /// Initializes a new instance of the class. /// - public class ParentalRating + /// The name. + /// The score. + public ParentalRating(string name, ParentalRatingScore? score) { - public ParentalRating() - { - } - - public ParentalRating(string name, int? value) - { - Name = name; - Value = value; - } - - /// - /// Gets or sets the name. - /// - /// The name. - public string Name { get; set; } - - /// - /// Gets or sets the value. - /// - /// The value. - public int? Value { get; set; } + Name = name; + Value = score?.Score; + RatingScore = score; } + + /// + /// Gets or sets the name. + /// + /// The name. + public string Name { get; set; } + + /// + /// Gets or sets the value. + /// + /// The value. + /// + /// Deprecated. + /// + public int? Value { get; set; } + + /// + /// Gets or sets the rating score. + /// + /// The rating score. + public ParentalRatingScore? RatingScore { get; set; } } diff --git a/MediaBrowser.Model/Entities/ParentalRatingEntry.cs b/MediaBrowser.Model/Entities/ParentalRatingEntry.cs new file mode 100644 index 0000000000..69be74ac0a --- /dev/null +++ b/MediaBrowser.Model/Entities/ParentalRatingEntry.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Model.Entities; + +/// +/// A class representing an parental rating entry. +/// +public class ParentalRatingEntry +{ + /// + /// Gets or sets the rating strings. + /// + [JsonPropertyName("ratingStrings")] + public required IReadOnlyList RatingStrings { get; set; } + + /// + /// Gets or sets the score. + /// + [JsonPropertyName("ratingScore")] + public required ParentalRatingScore RatingScore { get; set; } +} diff --git a/MediaBrowser.Model/Entities/ParentalRatingScore.cs b/MediaBrowser.Model/Entities/ParentalRatingScore.cs new file mode 100644 index 0000000000..b9bb99685f --- /dev/null +++ b/MediaBrowser.Model/Entities/ParentalRatingScore.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; + +namespace MediaBrowser.Model.Entities; + +/// +/// A class representing an parental rating score. +/// +public class ParentalRatingScore +{ + /// + /// Initializes a new instance of the class. + /// + /// The score. + /// The sub score. + public ParentalRatingScore(int score, int? subScore) + { + Score = score; + SubScore = subScore; + } + + /// + /// Gets or sets the score. + /// + [JsonPropertyName("score")] + public int Score { get; set; } + + /// + /// Gets or sets the sub score. + /// + [JsonPropertyName("subScore")] + public int? SubScore { get; set; } +} diff --git a/MediaBrowser.Model/Entities/ParentalRatingSystem.cs b/MediaBrowser.Model/Entities/ParentalRatingSystem.cs new file mode 100644 index 0000000000..b452f29018 --- /dev/null +++ b/MediaBrowser.Model/Entities/ParentalRatingSystem.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Model.Entities; + +/// +/// A class representing a parental rating system. +/// +public class ParentalRatingSystem +{ + /// + /// Gets or sets the country code. + /// + [JsonPropertyName("countryCode")] + public required string CountryCode { get; set; } + + /// + /// Gets or sets a value indicating whether sub scores are supported. + /// + [JsonPropertyName("supportsSubScores")] + public bool SupportsSubScores { get; set; } + + /// + /// Gets or sets the ratings. + /// + [JsonPropertyName("ratings")] + public IReadOnlyList? Ratings { get; set; } +} diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs index 20deaa5057..d9df95325c 100644 --- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs +++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs @@ -1,65 +1,64 @@ using System.Collections.Generic; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Model.Globalization +namespace MediaBrowser.Model.Globalization; + +/// +/// Interface ILocalizationManager. +/// +public interface ILocalizationManager { /// - /// Interface ILocalizationManager. + /// Gets the cultures. /// - public interface ILocalizationManager - { - /// - /// Gets the cultures. - /// - /// . - IEnumerable GetCultures(); + /// . + IEnumerable GetCultures(); - /// - /// Gets the countries. - /// - /// . - IEnumerable GetCountries(); + /// + /// Gets the countries. + /// + /// . + IReadOnlyList GetCountries(); - /// - /// Gets the parental ratings. - /// - /// . - IEnumerable GetParentalRatings(); + /// + /// Gets the parental ratings. + /// + /// . + IReadOnlyList GetParentalRatings(); - /// - /// Gets the rating level. - /// - /// The rating. - /// The optional two letter ISO language string. - /// or null. - int? GetRatingLevel(string rating, string? countryCode = null); + /// + /// Gets the rating level. + /// + /// The rating. + /// The optional two letter ISO language string. + /// or null. + ParentalRatingScore? GetRatingScore(string rating, string? countryCode = null); - /// - /// Gets the localized string. - /// - /// The phrase. - /// The culture. - /// . - string GetLocalizedString(string phrase, string culture); + /// + /// Gets the localized string. + /// + /// The phrase. + /// The culture. + /// . + string GetLocalizedString(string phrase, string culture); - /// - /// Gets the localized string. - /// - /// The phrase. - /// System.String. - string GetLocalizedString(string phrase); + /// + /// Gets the localized string. + /// + /// The phrase. + /// System.String. + string GetLocalizedString(string phrase); - /// - /// Gets the localization options. - /// - /// . - IEnumerable GetLocalizationOptions(); + /// + /// Gets the localization options. + /// + /// . + IEnumerable GetLocalizationOptions(); - /// - /// Returns the correct for the given language. - /// - /// The language. - /// The correct for the given language. - CultureDto? FindLanguageInfo(string language); - } + /// + /// Returns the correct for the given language. + /// + /// The language. + /// The correct for the given language. + CultureDto? FindLanguageInfo(string language); } diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index 49d7c0bcb0..6605064ad6 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -209,6 +209,7 @@ namespace MediaBrowser.Model.Querying ExternalEtag, PresentationUniqueKey, InheritedParentalRatingValue, + InheritedParentalRatingSubValue, ExternalSeriesId, SeriesPresentationUniqueKey, DateLastRefreshed, diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 3d430e1015..2c393ca862 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -111,6 +111,8 @@ namespace MediaBrowser.Model.Users /// The max parental rating. public int? MaxParentalRating { get; set; } + public int? MaxParentalSubRating { get; set; } + public string[] BlockedTags { get; set; } public string[] AllowedTags { get; set; } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index e8994693de..45f66f85fc 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -193,6 +193,7 @@ namespace MediaBrowser.Providers.Manager if (hasRefreshedMetadata && hasRefreshedImages) { item.DateLastRefreshed = DateTime.UtcNow; + updateType |= item.OnMetadataChanged(); } updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs index fc9695a091..332dec2e67 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs @@ -84,6 +84,8 @@ public class BaseItemEntity public int? InheritedParentalRatingValue { get; set; } + public int? InheritedParentalRatingSubValue { get; set; } + public string? UnratedType { get; set; } public float? CriticRating { get; set; } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs index 31538b5bf3..4da7074ec7 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs @@ -249,9 +249,14 @@ namespace Jellyfin.Database.Implementations.Entities public bool EnableUserPreferenceAccess { get; set; } /// - /// Gets or sets the maximum parental age rating. + /// Gets or sets the maximum parental rating score. /// - public int? MaxParentalAgeRating { get; set; } + public int? MaxParentalRatingScore { get; set; } + + /// + /// Gets or sets the maximum parental rating sub score. + /// + public int? MaxParentalRatingSubScore { get; set; } /// /// Gets or sets the remote client bitrate limit. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs new file mode 100644 index 0000000000..d6befbe5eb --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.Designer.cs @@ -0,0 +1,1658 @@ +// +using System; +using Jellyfin.Database.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20250326065026_AddInheritedParentalRatingSubValue")] + partial class AddInheritedParentalRatingSubValue + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingSubValue") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue") + .IsUnique(); + + b.ToTable("ItemValues"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingScore") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingSubScore") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "UserId", "CustomDataKey"); + + b.HasIndex("UserId"); + + b.HasIndex("ItemId", "UserId", "IsFavorite"); + + b.HasIndex("ItemId", "UserId", "LastPlayedDate"); + + b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("ItemId", "UserId", "Played"); + + b.ToTable("UserData"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Children") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem") + .WithMany("ParentAncestors") + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("UserData") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Navigation("Chapters"); + + b.Navigation("Children"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("ParentAncestors"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs new file mode 100644 index 0000000000..71f56a1492 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class AddInheritedParentalRatingSubValue : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "MaxParentalAgeRating", + table: "Users", + newName: "MaxParentalRatingScore"); + + migrationBuilder.AddColumn( + name: "MaxParentalRatingSubScore", + table: "Users", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "InheritedParentalRatingSubValue", + table: "BaseItems", + type: "INTEGER", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MaxParentalRatingSubScore", + table: "Users"); + + migrationBuilder.DropColumn( + name: "InheritedParentalRatingValue", + table: "BaseItems"); + + migrationBuilder.RenameColumn( + name: "MaxParentalRatingScore", + table: "Users", + newName: "MaxParentalAgeRating"); + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs index 5d8ddde0820763ca335545cb5f01cd5329d3eb95..8b2b26934579898be33a43311ef6a83ac8a97ccf 100644 GIT binary patch delta 2688 zcmbuBdq`7J9LL$6FI0SFsimd)M}%Zaiwy>uOP!Bt8yh}yEW66GkzME9(hQ^wtV~1u zVf9Zc5eB{zPqwrn^Ht_-zUcvi{Us?R6_E&0_nhhczw_7qo$vYG&pGG!xaW>_3V%%s zdld{Lih(cl$D5?PLDiK8L7E3J!+4=0;i>?P3t^%(wp_+>rDoP_DCG{vXv#_qX1z4K zT+dr-z1d>o3|w(i32Q0W#~g{iz?PKjK_HUzJk;nJ<^?&kGz@TdCqq1IfS>?xXz~9@ zi*xjz;c=ztN`Q@_WwlPHfjuu6jFx(ozw4o|P-7{C?~z)VdF+ioN5+OyE`U-+2R&#R zosv2MB?FfFGe`~+5BVXn^g1=K!J5U_SsW}!N%_3y-p}-dz>`LJMOP`w12U3YQ-Xql zztc2T*C+cagp5TR1gs5ax|`{M{Einb&uB|=^Bh@^1hAO9FaCc!vt9Xtw)~M@UFAuA zsF6@un~OvCyk`v5&(H)Mb!czX1;wH}CZuhZ&Czre&vu1Sa!{0IFKvRLdt5=pdMq8<*Use%do3 zWi$Brs`*QrE7la1nqc_peh4a)g=evqD%Pau%xuZltfkQH(3UPkX)O-gAi(U@>Fr8` zUWE!8^&#j|`x8%^isS!Aboi%aeL@N=T`SmhcAq7RRSuVX-nq<3q<(%?fNe)_kxNQ} zX-6!y_1pmaLJ%D7OT}`kNCEb~J=iQ5PDPQ(hrHA&QEtoYRylIM2qf}Se}Ri7M>Q|~ zh}2*$SZNU=;dliy-f{E0f#{PMm*u=X{ZH1>}u7{q!dE!->}-Dm2au2U^p04ka47r&^AZUVGq3ZmND1XyT{LONuYT% zft@4XUL(#cRmnT_-`HuR+O3?LB#PKUfD-2qsGU+^pV_J0Z65a?2OUYA7s)1Cr|&X& n3g2}01%IfS)8L_zeADZ(8D&W%Go$k=JXAI(;33j26bt?V{=Xn| delta 773 zcmY*VT}V@580MUl)1@e5R^$S!(M1=Lj=Zs@%{BA*Q)v{#(8?BiVNf@TMTsmD%s_o0 z1;t4uQBfZF`mwgyF)M#_6_vQczXifBVpBxWo4)gXIq2$pzvq44=Y8I1WlVbiL{f@G z(XtzAsRgG73ZS^VNu3lH;Ii3CoNk+r6>3roMv9YO2J7MgB8 zp1m2R?+)_NTQ4AzeC%JtK_YKO0luuF4JJ|jdI62u$74X(V2Xf#YoP{^%%M^NZMh|} zZrKOg_1bJPV9#)(fT&!|0O|zGBVj~G%PE#lZjPA*n!S@+Jb;o38ILD=(BnK2Bd1LM zp@E-ao<9c0X>B9}pX&t3HF9jl`d7Ns)3D+BD5k#+ljq;B3#fJTla;Ae%rtG(2XSHA z2cETq^aT$ISX1&eIdc9NmFf#F;q1Ev_%1!{sJb x.Name.Equals("TV-MA", StringComparison.Ordinal)); Assert.NotNull(tvma); - Assert.Equal(17, tvma!.Value); + Assert.Equal(17, tvma!.RatingScore!.Score); } [Fact] @@ -105,47 +105,49 @@ namespace Jellyfin.Server.Implementations.Tests.Localization var fsk = ratings.FirstOrDefault(x => x.Name.Equals("FSK-12", StringComparison.Ordinal)); Assert.NotNull(fsk); - Assert.Equal(12, fsk!.Value); + Assert.Equal(12, fsk!.RatingScore!.Score); } [Theory] - [InlineData("CA-R", "CA", 18)] - [InlineData("FSK-16", "DE", 16)] - [InlineData("FSK-18", "DE", 18)] - [InlineData("FSK-18", "US", 18)] - [InlineData("TV-MA", "US", 17)] - [InlineData("XXX", "asdf", 1000)] - [InlineData("Germany: FSK-18", "DE", 18)] - [InlineData("Rated : R", "US", 17)] - [InlineData("Rated: R", "US", 17)] - [InlineData("Rated R", "US", 17)] - [InlineData(" PG-13 ", "US", 13)] - public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel) + [InlineData("CA-R", "CA", 18, 1)] + [InlineData("FSK-16", "DE", 16, null)] + [InlineData("FSK-18", "DE", 18, null)] + [InlineData("FSK-18", "US", 18, null)] + [InlineData("TV-MA", "US", 17, 1)] + [InlineData("XXX", "asdf", 1000, null)] + [InlineData("Germany: FSK-18", "DE", 18, null)] + [InlineData("Rated : R", "US", 17, 0)] + [InlineData("Rated: R", "US", 17, 0)] + [InlineData("Rated R", "US", 17, 0)] + [InlineData(" PG-13 ", "US", 13, 0)] + public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int? expectedScore, int? expectedSubScore) { var localizationManager = Setup(new ServerConfiguration() { MetadataCountryCode = countryCode }); await localizationManager.LoadAll(); - var level = localizationManager.GetRatingLevel(value); - Assert.NotNull(level); - Assert.Equal(expectedLevel, level!); + var score = localizationManager.GetRatingScore(value); + Assert.NotNull(score); + Assert.Equal(expectedScore, score.Score); + Assert.Equal(expectedSubScore, score.SubScore); } [Theory] - [InlineData("0", 0)] - [InlineData("1", 1)] - [InlineData("6", 6)] - [InlineData("12", 12)] - [InlineData("42", 42)] - [InlineData("9999", 9999)] - public async Task GetRatingLevel_GivenValidAge_Success(string value, int expectedLevel) + [InlineData("0", 0, null)] + [InlineData("1", 1, null)] + [InlineData("6", 6, null)] + [InlineData("12", 12, null)] + [InlineData("42", 42, null)] + [InlineData("9999", 9999, null)] + public async Task GetRatingLevel_GivenValidAge_Success(string value, int? expectedScore, int? expectedSubScore) { var localizationManager = Setup(new ServerConfiguration { MetadataCountryCode = "nl" }); await localizationManager.LoadAll(); - var level = localizationManager.GetRatingLevel(value); - Assert.NotNull(level); - Assert.Equal(expectedLevel, level); + var score = localizationManager.GetRatingScore(value); + Assert.NotNull(score); + Assert.Equal(expectedScore, score.Score); + Assert.Equal(expectedSubScore, score.SubScore); } [Fact] @@ -156,10 +158,10 @@ namespace Jellyfin.Server.Implementations.Tests.Localization UICulture = "de-DE" }); await localizationManager.LoadAll(); - Assert.Null(localizationManager.GetRatingLevel("NR")); - Assert.Null(localizationManager.GetRatingLevel("unrated")); - Assert.Null(localizationManager.GetRatingLevel("Not Rated")); - Assert.Null(localizationManager.GetRatingLevel("n/a")); + Assert.Null(localizationManager.GetRatingScore("NR")); + Assert.Null(localizationManager.GetRatingScore("unrated")); + Assert.Null(localizationManager.GetRatingScore("Not Rated")); + Assert.Null(localizationManager.GetRatingScore("n/a")); } [Theory] @@ -173,7 +175,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization }); await localizationManager.LoadAll(); - Assert.Null(localizationManager.GetRatingLevel(value)); + Assert.Null(localizationManager.GetRatingScore(value)); } [Theory] From d9a79b5eefb2c2a15548f567349c39a5b46c64e2 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Mon, 31 Mar 2025 18:26:34 -0400 Subject: [PATCH 155/508] Add missing public properties to SystemInfo response --- Emby.Server.Implementations/SystemManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs index c4552474cb..5936df7f1a 100644 --- a/Emby.Server.Implementations/SystemManager.cs +++ b/Emby.Server.Implementations/SystemManager.cs @@ -53,6 +53,7 @@ public class SystemManager : ISystemManager HasPendingRestart = _applicationHost.HasPendingRestart, IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested, Version = _applicationHost.ApplicationVersionString, + ProductName = _applicationHost.Name, WebSocketPortNumber = _applicationHost.HttpPort, CompletedInstallations = _installationManager.CompletedInstallations.ToArray(), Id = _applicationHost.SystemId, @@ -65,6 +66,7 @@ public class SystemManager : ISystemManager TranscodingTempPath = _configurationManager.GetTranscodePath(), ServerName = _applicationHost.FriendlyName, LocalAddress = _applicationHost.GetSmartApiUrl(request), + StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted, SupportsLibraryMonitor = true, PackageName = _startupOptions.PackageName, CastReceiverApplications = _configurationManager.Configuration.CastReceiverApplications From e9729a536f26a92ccd30cfff4e698e66dbda1adc Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 1 Apr 2025 01:38:25 +0200 Subject: [PATCH 156/508] Use pattern matching for null checks (#13793) Fix the few that slipped through --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 2 +- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 2 +- MediaBrowser.Model/Extensions/ContainerHelper.cs | 4 ++-- MediaBrowser.Providers/TV/SeriesMetadataService.cs | 2 +- MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs | 2 +- MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs | 4 ++-- tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs | 4 ++-- tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index ca8ab0ef75..51291ec62c 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1753,7 +1753,7 @@ public class DynamicHlsController : BaseJellyfinApiController if (channels.HasValue && (channels.Value != 2 - || (state.AudioStream?.Channels != null && !useDownMixAlgorithm))) + || (state.AudioStream?.Channels is not null && !useDownMixAlgorithm))) { args += " -ac " + channels.Value; } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 207bb40d93..ed975af7fb 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -5621,7 +5621,7 @@ namespace MediaBrowser.Controller.MediaEncoding var doDeintH2645 = doDeintH264 || doDeintHevc; var doOclTonemap = IsHwTonemapAvailable(state, options); - var hasSubs = state.SubtitleStream != null && ShouldEncodeSubtitle(state); + var hasSubs = state.SubtitleStream is not null && ShouldEncodeSubtitle(state); var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; var hasAssSubs = hasSubs diff --git a/MediaBrowser.Model/Extensions/ContainerHelper.cs b/MediaBrowser.Model/Extensions/ContainerHelper.cs index 39e5358bac..848cc2f621 100644 --- a/MediaBrowser.Model/Extensions/ContainerHelper.cs +++ b/MediaBrowser.Model/Extensions/ContainerHelper.cs @@ -21,7 +21,7 @@ public static class ContainerHelper public static bool ContainsContainer(string? profileContainers, string? inputContainer) { var isNegativeList = false; - if (profileContainers != null && profileContainers.StartsWith('-')) + if (profileContainers is not null && profileContainers.StartsWith('-')) { isNegativeList = true; profileContainers = profileContainers[1..]; @@ -42,7 +42,7 @@ public static class ContainerHelper public static bool ContainsContainer(string? profileContainers, ReadOnlySpan inputContainer) { var isNegativeList = false; - if (profileContainers != null && profileContainers.StartsWith('-')) + if (profileContainers is not null && profileContainers.StartsWith('-')) { isNegativeList = true; profileContainers = profileContainers[1..]; diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 284415dce6..42d59d3483 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -55,7 +55,7 @@ namespace MediaBrowser.Providers.TV foreach (var season in seasons) { - var hasUpdate = refreshOptions != null && season.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata); + var hasUpdate = refreshOptions is not null && season.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata); if (hasUpdate) { await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index 2a1a14834b..19b1bbe7b6 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -95,7 +95,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers originalTitle.Append(" / ").Append(additionalEpisode.Item.OriginalTitle); } - if (additionalEpisode.Item.IndexNumber != null) + if (additionalEpisode.Item.IndexNumber is not null) { item.Item.IndexNumberEnd = Math.Max((int)additionalEpisode.Item.IndexNumber, item.Item.IndexNumberEnd ?? (int)additionalEpisode.Item.IndexNumber); } diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 4c8a54cc98..0217bded13 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -1020,12 +1020,12 @@ namespace MediaBrowser.XbmcMetadata.Savers protected static string SortNameOrName(BaseItem item) { - if (item == null) + if (item is null) { return string.Empty; } - if (item.SortName != null) + if (item.SortName is not null) { string trimmed = item.SortName.Trim(); if (trimmed.Length > 0) diff --git a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs index a74dab5f29..e95df16354 100644 --- a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs @@ -87,7 +87,7 @@ public class UserControllerTests Assert.Contains( Validate(userPolicy), v => v.MemberNames.Contains("PasswordResetProviderId") && - v.ErrorMessage != null && + v.ErrorMessage is not null && v.ErrorMessage.Contains("required", StringComparison.CurrentCultureIgnoreCase)); } @@ -105,7 +105,7 @@ public class UserControllerTests Assert.Contains(Validate(userPolicy), v => v.MemberNames.Contains("AuthenticationProviderId") && - v.ErrorMessage != null && + v.ErrorMessage is not null && v.ErrorMessage.Contains("required", StringComparison.CurrentCultureIgnoreCase)); } diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs index 86819de8c0..8dea468064 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs @@ -31,7 +31,7 @@ public class StreamInfoTests /// An of . private static object? RandomArray(Random random, Type? elementType) { - if (elementType == null) + if (elementType is null) { return null; } @@ -148,7 +148,7 @@ public class StreamInfoTests var type = property.PropertyType; // If nullable, then set it to null, 25% of the time. - if (Nullable.GetUnderlyingType(type) != null) + if (Nullable.GetUnderlyingType(type) is not null) { if (random.Next(0, 4) == 0) { From 4a4fef830eccf0629d7cf955126f0cd78867e0ee Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 1 Apr 2025 07:43:31 +0800 Subject: [PATCH 157/508] Explicitly set default value for enums used in API models (#13821) Enums in response model with no nullability or default value will make the API very fragile as each extension to the enum will break the API for some clients, but a lot of enums actually do have an unknown value which should be used as a default. This set all model properties that are non-nullable using an enum that has an Unknown member in 10.10, except MediaStream.VideoRangeType which is refactored in #13277 --- MediaBrowser.Model/Dto/BaseItemDto.cs | 2 ++ MediaBrowser.Model/Dto/BaseItemPerson.cs | 2 ++ MediaBrowser.Model/Entities/MediaStream.cs | 1 + MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs | 2 ++ MediaBrowser.Model/Search/SearchHint.cs | 2 ++ 5 files changed, 9 insertions(+) diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 7bfd8ca29c..b38763fbf7 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.Drawing; @@ -586,6 +587,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the type of the media. /// /// The type of the media. + [DefaultValue(MediaType.Unknown)] public MediaType MediaType { get; set; } /// diff --git a/MediaBrowser.Model/Dto/BaseItemPerson.cs b/MediaBrowser.Model/Dto/BaseItemPerson.cs index d3bcf492d8..80e2cfb08d 100644 --- a/MediaBrowser.Model/Dto/BaseItemPerson.cs +++ b/MediaBrowser.Model/Dto/BaseItemPerson.cs @@ -1,6 +1,7 @@ #nullable disable using System; using System.Collections.Generic; +using System.ComponentModel; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; using MediaBrowser.Model.Entities; @@ -34,6 +35,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the type. /// /// The type. + [DefaultValue(PersonKind.Unknown)] public PersonKind Type { get; set; } /// diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 400768ef34..dae3d84ae6 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -157,6 +157,7 @@ namespace MediaBrowser.Model.Entities /// Gets the video range. /// /// The video range. + [DefaultValue(VideoRange.Unknown)] public VideoRange VideoRange { get diff --git a/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs b/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs index 6e5c7885cc..d9129c3957 100644 --- a/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs +++ b/MediaBrowser.Model/MediaSegments/MediaSegmentDto.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Jellyfin.Database.Implementations.Enums; namespace MediaBrowser.Model.MediaSegments; @@ -21,6 +22,7 @@ public class MediaSegmentDto /// /// Gets or sets the type of content this segment defines. /// + [DefaultValue(MediaSegmentType.Unknown)] public MediaSegmentType Type { get; set; } /// diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs index 2e2979fcf0..a18a813cc5 100644 --- a/MediaBrowser.Model/Search/SearchHint.cs +++ b/MediaBrowser.Model/Search/SearchHint.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Jellyfin.Data.Enums; namespace MediaBrowser.Model.Search @@ -115,6 +116,7 @@ namespace MediaBrowser.Model.Search /// Gets or sets the type of the media. /// /// The type of the media. + [DefaultValue(MediaType.Unknown)] public MediaType MediaType { get; set; } /// From 5769c398c6bf5e23bac4063cdb17859b10fe37c7 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Tue, 1 Apr 2025 01:44:06 +0200 Subject: [PATCH 158/508] Fix Tmdb external URL generation (#13817) * Fix Tmdb external URL generation * Update MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs --- .../Plugins/Tmdb/TmdbExternalUrlProvider.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs index bec800c035..27e3f93a3d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs @@ -33,17 +33,18 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider if (season.Series.TryGetProviderId(MetadataProvider.Tmdb, out var seriesExternalId)) { var orderString = season.Series.DisplayOrder; - if (string.IsNullOrEmpty(orderString)) + var seasonNumber = season.IndexNumber; + if (string.IsNullOrEmpty(orderString) && seasonNumber is not null) { // Default order is airdate - yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{season.IndexNumber}"; + yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{seasonNumber}"; } if (Enum.TryParse(season.Series.DisplayOrder, out var order)) { - if (order.Equals(TvGroupType.OriginalAirDate)) + if (order.Equals(TvGroupType.OriginalAirDate) && seasonNumber is not null) { - yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{season.IndexNumber}"; + yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{seasonNumber}"; } } } @@ -53,17 +54,19 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider if (episode.Series.TryGetProviderId(MetadataProvider.Imdb, out seriesExternalId)) { var orderString = episode.Series.DisplayOrder; - if (string.IsNullOrEmpty(orderString)) + var seasonNumber = episode.Season?.IndexNumber; + var episodeNumber = episode.IndexNumber; + if (string.IsNullOrEmpty(orderString) && seasonNumber is not null && episodeNumber is not null) { // Default order is airdate - yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{episode.Season.IndexNumber}/episode/{episode.IndexNumber}"; + yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{seasonNumber}/episode/{episodeNumber}"; } if (Enum.TryParse(orderString, out var order)) { - if (order.Equals(TvGroupType.OriginalAirDate)) + if (order.Equals(TvGroupType.OriginalAirDate) && seasonNumber is not null && episodeNumber is not null) { - yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{episode.Season.IndexNumber}/episode/{episode.IndexNumber}"; + yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{seasonNumber}/episode/{episodeNumber}"; } } } From 2b742a59665e5579c4ac4d38a62f95aa9ff5365a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Tue, 1 Apr 2025 01:45:03 +0200 Subject: [PATCH 159/508] Reduce SKImage to SKBitmap conversion, high quality canvas (#5366) --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 20 +++++++------------ .../StripCollageBuilder.cs | 18 +++++++++++------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 99f7fa7f96..73c8c39663 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -557,20 +557,14 @@ public class SkiaEncoder : IImageEncoder canvas.Clear(SKColor.Parse(options.BackgroundColor)); } + using var paint = new SKPaint(); // Add blur if option is present - if (blur > 0) - { - // create image from resized bitmap to apply blur - using var paint = new SKPaint(); - using var filter = SKImageFilter.CreateBlur(blur, blur); - paint.ImageFilter = filter; - canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint); - } - else - { - // draw resized bitmap onto canvas - canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height)); - } + using var filter = blur > 0 ? SKImageFilter.CreateBlur(blur, blur) : null; + paint.FilterQuality = SKFilterQuality.High; + paint.ImageFilter = filter; + + // create image from resized bitmap to apply blur + canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint); // If foreground layer present then draw if (hasForegroundColor) diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index b0c9c0b3cc..03e202e5a4 100644 --- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -109,15 +109,18 @@ public partial class StripCollageBuilder // resize to the same aspect as the original var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width); - using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace)); + using var resizedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace)); + using var paint = new SKPaint(); + paint.FilterQuality = SKFilterQuality.High; // draw the backdrop - canvas.DrawImage(residedBackdrop, 0, 0); + canvas.DrawImage(resizedBackdrop, 0, 0, paint); // draw shadow rectangle using var paintColor = new SKPaint { Color = SKColors.Black.WithAlpha(0x78), - Style = SKPaintStyle.Fill + Style = SKPaintStyle.Fill, + FilterQuality = SKFilterQuality.High }; canvas.DrawRect(0, 0, width, height, paintColor); @@ -131,7 +134,8 @@ public partial class StripCollageBuilder TextSize = 112, TextAlign = SKTextAlign.Left, Typeface = typeFace, - IsAntialias = true + IsAntialias = true, + FilterQuality = SKFilterQuality.High }; // scale down text to 90% of the width if text is larger than 95% of the width @@ -188,14 +192,16 @@ public partial class StripCollageBuilder continue; } - // Scale image. The FromBitmap creates a copy + // Scale image var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace); using var resizeImage = SkiaEncoder.ResizeImage(currentBitmap, imageInfo); + using var paint = new SKPaint(); + paint.FilterQuality = SKFilterQuality.High; // draw this image into the strip at the next position var xPos = x * cellWidth; var yPos = y * cellHeight; - canvas.DrawImage(resizeImage, xPos, yPos); + canvas.DrawImage(resizeImage, xPos, yPos, paint); } } From 940c4e8ba895f02fdc2f04932cd1eea9f7cc8cbf Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> Date: Tue, 1 Apr 2025 02:45:14 +0300 Subject: [PATCH 160/508] Add Dolby Vision tests for Tizen (#12670) * Fix Tizen H264 profiles * Add Dolby Vision tests for Tizen * Allow Dolby Vision fallback layer on Tizen 3+ --- .../Dlna/StreamBuilderTests.cs | 8 ++++++++ .../Test Data/DeviceProfile-Tizen3-stereo.json | 16 +++++++++++++++- .../Test Data/DeviceProfile-Tizen4-4K-5.1.json | 16 +++++++++++++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index ae9edd3867..2c1080ffe3 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -182,6 +182,10 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen3-stereo", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] + [InlineData("Tizen3-stereo", "mkv-dvhe.08-eac3-15200k", PlayMethod.DirectPlay)] + [InlineData("Tizen3-stereo", "mp4-dvh1.05-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioChannelsNotSupported, "Transcode")] + [InlineData("Tizen3-stereo", "mp4-dvhe.08-eac3-15200k", PlayMethod.DirectPlay)] + [InlineData("Tizen3-stereo", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.VideoBitrateNotSupported | TranscodeReason.VideoRangeTypeNotSupported | TranscodeReason.AudioChannelsNotSupported, "Transcode")] [InlineData("Tizen3-stereo", "numstreams-32", PlayMethod.DirectPlay)] [InlineData("Tizen3-stereo", "numstreams-33", PlayMethod.Transcode, TranscodeReason.StreamCountExceedsLimit, "Remux")] // Tizen 4 4K 5.1 @@ -195,6 +199,10 @@ namespace Jellyfin.Model.Tests [InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] + [InlineData("Tizen4-4K-5.1", "mkv-dvhe.08-eac3-15200k", PlayMethod.DirectPlay)] + [InlineData("Tizen4-4K-5.1", "mp4-dvh1.05-eac3-15200k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported, "Transcode")] + [InlineData("Tizen4-4K-5.1", "mp4-dvhe.08-eac3-15200k", PlayMethod.DirectPlay)] + [InlineData("Tizen4-4K-5.1", "mkv-dvhe.05-eac3-28000k", PlayMethod.Transcode, TranscodeReason.VideoRangeTypeNotSupported, "Transcode")] [InlineData("Tizen4-4K-5.1", "numstreams-32", PlayMethod.DirectPlay)] [InlineData("Tizen4-4K-5.1", "numstreams-33", PlayMethod.Transcode, TranscodeReason.StreamCountExceedsLimit, "Remux")] // WebOS 23 diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json index 895d13f074..9d43d2166d 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json @@ -439,7 +439,14 @@ { "Condition": "EqualsAny", "Property": "VideoProfile", - "Value": "high|main|baseline|constrained baseline|high 10", + "Value": "high|main|baseline|constrained baseline", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR", "IsRequired": false, "$type": "ProfileCondition" }, @@ -478,6 +485,13 @@ "IsRequired": false, "$type": "ProfileCondition" }, + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR|DOVIWithSDR|HDR10|DOVIWithHDR10|HLG|DOVIWithHLG", + "IsRequired": false, + "$type": "ProfileCondition" + }, { "Condition": "LessThanEqual", "Property": "VideoLevel", diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json index 345d387251..3859ef9941 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json @@ -439,7 +439,14 @@ { "Condition": "EqualsAny", "Property": "VideoProfile", - "Value": "high|main|baseline|constrained baseline|high 10", + "Value": "high|main|baseline|constrained baseline", + "IsRequired": false, + "$type": "ProfileCondition" + }, + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR", "IsRequired": false, "$type": "ProfileCondition" }, @@ -471,6 +478,13 @@ "IsRequired": false, "$type": "ProfileCondition" }, + { + "Condition": "EqualsAny", + "Property": "VideoRangeType", + "Value": "SDR|DOVIWithSDR|HDR10|DOVIWithHDR10|HLG|DOVIWithHLG", + "IsRequired": false, + "$type": "ProfileCondition" + }, { "Condition": "LessThanEqual", "Property": "VideoLevel", From 14b785d188647ae896c5d20869ab6bcc967fcfc2 Mon Sep 17 00:00:00 2001 From: KGT1 Date: Tue, 1 Apr 2025 01:46:01 +0200 Subject: [PATCH 161/508] Preserve SplashscreenLocation when updating branding config (#13756) * add BrandingOptionsDto and add branding endpoints * refactor new HttpGet Configuration Branding into existing API calls * Add BrandingOptions to _ignoredConfigurations for openAPI * rename BrandOptionsDto to BrandingOptionsDto --- .../Controllers/BrandingController.cs | 13 ++++++++-- .../Controllers/ConfigurationController.cs | 25 +++++++++++++++++++ .../Filters/AdditionalModelFilter.cs | 2 +- .../Branding/BrandingOptions.cs | 7 ------ .../Branding/BrandingOptionsDto.cs | 25 +++++++++++++++++++ 5 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 MediaBrowser.Model/Branding/BrandingOptionsDto.cs diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs index 3c2c4b4dbd..1d948ff206 100644 --- a/Jellyfin.Api/Controllers/BrandingController.cs +++ b/Jellyfin.Api/Controllers/BrandingController.cs @@ -29,9 +29,18 @@ public class BrandingController : BaseJellyfinApiController /// An containing the branding configuration. [HttpGet("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetBrandingOptions() + public ActionResult GetBrandingOptions() { - return _serverConfigurationManager.GetConfiguration("branding"); + var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); + + var brandingOptionsDto = new BrandingOptionsDto + { + LoginDisclaimer = brandingOptions.LoginDisclaimer, + CustomCss = brandingOptions.CustomCss, + SplashscreenEnabled = brandingOptions.SplashscreenEnabled + }; + + return brandingOptionsDto; } /// diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index abe8bec2db..8dcaebf6db 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -9,6 +9,7 @@ using Jellyfin.Extensions.Json; using MediaBrowser.Common.Api; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Branding; using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -119,6 +120,30 @@ public class ConfigurationController : BaseJellyfinApiController return new MetadataOptions(); } + /// + /// Updates branding configuration. + /// + /// Branding configuration. + /// Branding configuration updated. + /// Update status. + [HttpPost("Configuration/Branding")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult UpdateBrandingConfiguration([FromBody, Required] BrandingOptionsDto configuration) + { + // Get the current branding configuration to preserve SplashscreenLocation + var currentBranding = (BrandingOptions)_configurationManager.GetConfiguration("branding"); + + // Update only the properties from BrandingOptionsDto + currentBranding.LoginDisclaimer = configuration.LoginDisclaimer; + currentBranding.CustomCss = configuration.CustomCss; + currentBranding.SplashscreenEnabled = configuration.SplashscreenEnabled; + + _configurationManager.SaveConfiguration("branding", currentBranding); + + return NoContent(); + } + /// /// Updates the path to the media encoder. /// diff --git a/Jellyfin.Server/Filters/AdditionalModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs index bf38f741cd..4cd0fc231e 100644 --- a/Jellyfin.Server/Filters/AdditionalModelFilter.cs +++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs @@ -25,7 +25,7 @@ namespace Jellyfin.Server.Filters public class AdditionalModelFilter : IDocumentFilter { // Array of options that should not be visible in the api spec. - private static readonly Type[] _ignoredConfigurations = { typeof(MigrationOptions) }; + private static readonly Type[] _ignoredConfigurations = { typeof(MigrationOptions), typeof(MediaBrowser.Model.Branding.BrandingOptions) }; private readonly IServerConfigurationManager _serverConfigurationManager; /// diff --git a/MediaBrowser.Model/Branding/BrandingOptions.cs b/MediaBrowser.Model/Branding/BrandingOptions.cs index c6580598b4..5ec6b0dd4b 100644 --- a/MediaBrowser.Model/Branding/BrandingOptions.cs +++ b/MediaBrowser.Model/Branding/BrandingOptions.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace MediaBrowser.Model.Branding; /// @@ -27,10 +25,5 @@ public class BrandingOptions /// /// Gets or sets the splashscreen location on disk. /// - /// - /// Not served via the API. - /// Only used to save the custom uploaded user splashscreen in the configuration file. - /// - [JsonIgnore] public string? SplashscreenLocation { get; set; } } diff --git a/MediaBrowser.Model/Branding/BrandingOptionsDto.cs b/MediaBrowser.Model/Branding/BrandingOptionsDto.cs new file mode 100644 index 0000000000..c0d8cb31c2 --- /dev/null +++ b/MediaBrowser.Model/Branding/BrandingOptionsDto.cs @@ -0,0 +1,25 @@ +namespace MediaBrowser.Model.Branding; + +/// +/// The branding options DTO for API use. +/// This DTO excludes SplashscreenLocation to prevent it from being updated via API. +/// +public class BrandingOptionsDto +{ + /// + /// Gets or sets the login disclaimer. + /// + /// The login disclaimer. + public string? LoginDisclaimer { get; set; } + + /// + /// Gets or sets the custom CSS. + /// + /// The custom CSS. + public string? CustomCss { get; set; } + + /// + /// Gets or sets a value indicating whether to enable the splashscreen. + /// + public bool SplashscreenEnabled { get; set; } = false; +} From 086fbd49cfba3dcdb27ba8b37ff25722e9b37fb4 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Tue, 1 Apr 2025 01:46:21 +0200 Subject: [PATCH 162/508] Cleanup ItemFields (#13818) * Cleanup ItemFields * Update MediaBrowser.Model/Querying/ItemFields.cs --- Emby.Server.Implementations/Dto/DtoService.cs | 21 +++-- .../Session/SessionManager.cs | 1 - MediaBrowser.Controller/Dto/DtoOptions.cs | 2 +- MediaBrowser.Model/Querying/ItemFields.cs | 80 ++++++++++++------- 4 files changed, 63 insertions(+), 41 deletions(-) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 0ce967e6a9..5b0fc9ef3b 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -41,7 +41,6 @@ namespace Emby.Server.Implementations.Dto private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly IUserDataManager _userDataRepository; - private readonly IItemRepository _itemRepo; private readonly IImageProcessor _imageProcessor; private readonly IProviderManager _providerManager; @@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.Dto ILogger logger, ILibraryManager libraryManager, IUserDataManager userDataRepository, - IItemRepository itemRepo, IImageProcessor imageProcessor, IProviderManager providerManager, IRecordingsManager recordingsManager, @@ -71,7 +69,6 @@ namespace Emby.Server.Implementations.Dto _logger = logger; _libraryManager = libraryManager; _userDataRepository = userDataRepository; - _itemRepo = itemRepo; _imageProcessor = imageProcessor; _providerManager = providerManager; _recordingsManager = recordingsManager; @@ -99,11 +96,11 @@ namespace Emby.Server.Implementations.Dto if (item is LiveTvChannel tvChannel) { - (channelTuples ??= new()).Add((dto, tvChannel)); + (channelTuples ??= []).Add((dto, tvChannel)); } else if (item is LiveTvProgram) { - (programTuples ??= new()).Add((item, dto)); + (programTuples ??= []).Add((item, dto)); } if (item is IItemByName byName) @@ -590,12 +587,12 @@ namespace Emby.Server.Implementations.Dto if (dto.ImageBlurHashes is not null) { // Only add BlurHash for the person's image. - baseItemPerson.ImageBlurHashes = new Dictionary>(); + baseItemPerson.ImageBlurHashes = []; foreach (var (imageType, blurHash) in dto.ImageBlurHashes) { if (blurHash is not null) { - baseItemPerson.ImageBlurHashes[imageType] = new Dictionary(); + baseItemPerson.ImageBlurHashes[imageType] = []; foreach (var (imageId, blurHashValue) in blurHash) { if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase)) @@ -674,11 +671,11 @@ namespace Emby.Server.Implementations.Dto if (!string.IsNullOrEmpty(image.BlurHash)) { - dto.ImageBlurHashes ??= new Dictionary>(); + dto.ImageBlurHashes ??= []; if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value)) { - value = new Dictionary(); + value = []; dto.ImageBlurHashes[image.Type] = value; } @@ -709,7 +706,7 @@ namespace Emby.Server.Implementations.Dto if (hashes.Count > 0) { - dto.ImageBlurHashes ??= new Dictionary>(); + dto.ImageBlurHashes ??= []; dto.ImageBlurHashes[imageType] = hashes; } @@ -756,7 +753,7 @@ namespace Emby.Server.Implementations.Dto dto.AspectRatio = hasAspectRatio.AspectRatio; } - dto.ImageBlurHashes = new Dictionary>(); + dto.ImageBlurHashes = []; var backdropLimit = options.GetImageLimit(ImageType.Backdrop); if (backdropLimit > 0) @@ -772,7 +769,7 @@ namespace Emby.Server.Implementations.Dto if (options.EnableImages) { - dto.ImageTags = new Dictionary(); + dto.ImageTags = []; // Prevent implicitly captured closure var currentItem = item; diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index ac3e105940..924f502860 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1808,7 +1808,6 @@ namespace Emby.Server.Implementations.Session fields.Remove(ItemFields.DateLastSaved); fields.Remove(ItemFields.DisplayPreferencesId); fields.Remove(ItemFields.Etag); - fields.Remove(ItemFields.InheritedParentalRatingValue); fields.Remove(ItemFields.ItemCounts); fields.Remove(ItemFields.MediaSourceCount); fields.Remove(ItemFields.MediaStreams); diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs index cb638cf90b..a71cdbd62c 100644 --- a/MediaBrowser.Controller/Dto/DtoOptions.cs +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Dto EnableUserData = true; AddCurrentProgram = true; - Fields = allFields ? AllItemFields : Array.Empty(); + Fields = allFields ? AllItemFields : []; ImageTypes = AllImageTypes; } diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index 6605064ad6..ffecd392f2 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -1,7 +1,3 @@ -#pragma warning disable CS1591 - -using System; - namespace MediaBrowser.Model.Querying { /// @@ -39,6 +35,9 @@ namespace MediaBrowser.Model.Querying /// Trickplay, + /// + /// The child count. + /// ChildCount, /// @@ -81,11 +80,6 @@ namespace MediaBrowser.Model.Querying /// Genres, - /// - /// The home page URL. - /// - HomePageUrl, - /// /// The item counts. /// @@ -101,6 +95,9 @@ namespace MediaBrowser.Model.Querying /// MediaSources, + /// + /// The original title. + /// OriginalTitle, /// @@ -123,6 +120,9 @@ namespace MediaBrowser.Model.Querying /// People, + /// + /// Value indicating whether playback access is granted. + /// PlayAccess, /// @@ -140,6 +140,9 @@ namespace MediaBrowser.Model.Querying /// PrimaryImageAspectRatio, + /// + /// The recursive item count. + /// RecursiveItemCount, /// @@ -147,14 +150,6 @@ namespace MediaBrowser.Model.Querying /// Settings, - /// - /// The screenshot image tags. - /// - [Obsolete("Screenshot image type is no longer used.")] - ScreenshotImageTags, - - SeriesPrimaryImage, - /// /// The series studio. /// @@ -201,27 +196,58 @@ namespace MediaBrowser.Model.Querying SeasonUserData, /// - /// The service name. + /// The last time metadata was refreshed. /// - ServiceName, - ThemeSongIds, - ThemeVideoIds, - ExternalEtag, - PresentationUniqueKey, - InheritedParentalRatingValue, - InheritedParentalRatingSubValue, - ExternalSeriesId, - SeriesPresentationUniqueKey, DateLastRefreshed, + + /// + /// The last time metadata was saved. + /// DateLastSaved, + + /// + /// The refresh state. + /// RefreshState, + + /// + /// The channel image. + /// ChannelImage, + + /// + /// Value indicating whether media source display is enabled. + /// EnableMediaSourceDisplay, + + /// + /// The width. + /// Width, + + /// + /// The height. + /// Height, + + /// + /// The external Ids. + /// ExtraIds, + + /// + /// The local trailer count. + /// LocalTrailerCount, + + /// + /// Value indicating whether the item is HD. + /// IsHD, + + /// + /// The special feature count. + /// SpecialFeatureCount } } From 9c7cf808aa10b44677f641caccc3b8f409a419fd Mon Sep 17 00:00:00 2001 From: KGT1 Date: Thu, 3 Apr 2025 00:32:53 +0200 Subject: [PATCH 163/508] allow admin users to get Splashscreen even when disabled (#13825) refactor --- Jellyfin.Api/Controllers/ImageController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index e7b7405ca9..abda053d36 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -1727,7 +1727,8 @@ public class ImageController : BaseJellyfinApiController [FromQuery, Range(0, 100)] int quality = 90) { var brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); - if (!brandingOptions.SplashscreenEnabled) + var isAdmin = User.IsInRole(Constants.UserRoles.Administrator); + if (!brandingOptions.SplashscreenEnabled && !isAdmin) { return NotFound(); } From 49ac705867234c48e79ceb1cd84bc4394c65313d Mon Sep 17 00:00:00 2001 From: gnattu Date: Thu, 3 Apr 2025 08:06:02 +0800 Subject: [PATCH 164/508] Improve dynamic HDR metadata handling (#13277) * Add support for bitstream filter to remove dynamic hdr metadata * Add support for ffprobe's only_first_vframe for HDR10+ detection * Add BitStreamFilterOptionType for metadata removal check * Map HDR10+ metadata to VideoRangeType.cs Current implementation uses a hack that abuses the EL flag to avoid database schema changes. Should add proper field once EFCore migration is merged. * Add more Dolby Vision Range types Out of spec ones are problematic and should be marked as a dedicated invalid type and handled by the server to not crash the player. Profile 7 videos should not be treated as normal HDR10 videos at all and should remove the metadata before serving. * Remove dynamic hdr metadata when necessary * Allow direct playback of HDR10+ videos on HDR10 clients * Only use dovi codec tag when dovi metadata is not removed * Handle DV Profile 7 Videos better * Fix HDR10+ with new bitmask * Indicate the presence of HDR10+ in HLS SUPPLEMENTAL-CODECS * Fix Dovi 8.4 not labeled as HLG in HLS * Fallback to dovi_rpu bsf for av1 when possible * Fix dovi_rpu cli for av1 * Use correct EFCore db column for HDR10+ * Undo outdated migration * Add proper hdr10+ migration * Remove outdated migration * Rebase to new db code * Add migrations for Hdr10PlusPresentFlag * Directly use bsf enum * Add xmldocs for SupportsBitStreamFilterWithOption * Make `VideoRangeType.Unknown` explicitly default on api models. * Unset default for non-api model class * Use tuples for bsf dictionary for now --- .../Controllers/DynamicHlsController.cs | 9 +- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 87 +- Jellyfin.Data/Enums/VideoRangeType.cs | 21 + .../Item/MediaStreamRepository.cs | 4 +- .../BitStreamFilterOptionType.cs | 32 + .../MediaEncoding/EncodingHelper.cs | 211 ++- .../MediaEncoding/EncodingJobInfo.cs | 1 + .../MediaEncoding/IMediaEncoder.cs | 7 + .../Encoder/EncoderValidator.cs | 46 + .../Encoder/MediaEncoder.cs | 20 + .../Probing/InternalMediaInfoResult.cs | 7 + .../Probing/MediaFrameInfo.cs | 184 ++ .../Probing/MediaFrameSideDataInfo.cs | 16 + .../Probing/ProbeResultNormalizer.cs | 15 +- MediaBrowser.Model/Dlna/ConditionProcessor.cs | 9 + MediaBrowser.Model/Dlna/StreamInfo.cs | 1 + MediaBrowser.Model/Entities/MediaStream.cs | 37 +- .../Entities/MediaStreamInfo.cs | 2 + ...0250327171413_AddHdr10PlusFlag.Designer.cs | 1655 +++++++++++++++++ .../20250327171413_AddHdr10PlusFlag.cs | 28 + .../Migrations/JellyfinDbModelSnapshot.cs | Bin 58741 -> 58855 bytes 21 files changed, 2325 insertions(+), 67 deletions(-) create mode 100644 MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs create mode 100644 MediaBrowser.MediaEncoding/Probing/MediaFrameInfo.cs create mode 100644 MediaBrowser.MediaEncoding/Probing/MediaFrameSideDataInfo.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.Designer.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 51291ec62c..31b96972ea 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1675,7 +1675,7 @@ public class DynamicHlsController : BaseJellyfinApiController } var audioCodec = _encodingHelper.GetAudioEncoder(state); - var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); + var bitStreamArgs = _encodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); // opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer var strictArgs = string.Empty; @@ -1822,10 +1822,11 @@ public class DynamicHlsController : BaseJellyfinApiController // Clients reporting Dolby Vision capabilities with fallbacks may only support the fallback layer. // Only enable Dolby Vision remuxing if the client explicitly declares support for profiles without fallbacks. var clientSupportsDoVi = requestedRange.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase); - var videoIsDoVi = state.VideoStream.VideoRangeType is VideoRangeType.DOVI or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG or VideoRangeType.DOVIWithSDR; + var videoIsDoVi = EncodingHelper.IsDovi(state.VideoStream); if (EncodingHelper.IsCopyCodec(codec) - && (videoIsDoVi && clientSupportsDoVi)) + && (videoIsDoVi && clientSupportsDoVi) + && !_encodingHelper.IsDoviRemoved(state)) { if (isActualOutputVideoCodecHevc) { @@ -1855,7 +1856,7 @@ public class DynamicHlsController : BaseJellyfinApiController // If h264_mp4toannexb is ever added, do not use it for live tv. if (state.VideoStream is not null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { - string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream); + string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state, MediaStreamType.Video); if (!string.IsNullOrEmpty(bitStreamArgs)) { args += " " + bitStreamArgs; diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index ebd0288ca6..a38ad379cc 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -345,13 +345,15 @@ public class DynamicHlsHelper if (videoRange == VideoRange.HDR) { - if (videoRangeType == VideoRangeType.HLG) + switch (videoRangeType) { - builder.Append(",VIDEO-RANGE=HLG"); - } - else - { - builder.Append(",VIDEO-RANGE=PQ"); + case VideoRangeType.HLG: + case VideoRangeType.DOVIWithHLG: + builder.Append(",VIDEO-RANGE=HLG"); + break; + default: + builder.Append(",VIDEO-RANGE=PQ"); + break; } } } @@ -418,36 +420,67 @@ public class DynamicHlsHelper /// StreamState of the current stream. private void AppendPlaylistSupplementalCodecsField(StringBuilder builder, StreamState state) { - // Dolby Vision currently cannot exist when transcoding + // HDR dynamic metadata currently cannot exist when transcoding if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) { return; } - var dvProfile = state.VideoStream.DvProfile; - var dvLevel = state.VideoStream.DvLevel; - var dvRangeString = state.VideoStream.VideoRangeType switch + if (EncodingHelper.IsDovi(state.VideoStream) && !_encodingHelper.IsDoviRemoved(state)) { - VideoRangeType.DOVIWithHDR10 => "db1p", - VideoRangeType.DOVIWithHLG => "db4h", - _ => string.Empty - }; - - if (dvProfile is null || dvLevel is null || string.IsNullOrEmpty(dvRangeString)) + AppendDvString(); + } + else if (EncodingHelper.IsHdr10Plus(state.VideoStream) && !_encodingHelper.IsHdr10PlusRemoved(state)) { - return; + AppendHdr10PlusString(); } - var dvFourCc = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" : "dvh1"; - builder.Append(",SUPPLEMENTAL-CODECS=\"") - .Append(dvFourCc) - .Append('.') - .Append(dvProfile.Value.ToString("D2", CultureInfo.InvariantCulture)) - .Append('.') - .Append(dvLevel.Value.ToString("D2", CultureInfo.InvariantCulture)) - .Append('/') - .Append(dvRangeString) - .Append('"'); + return; + + void AppendDvString() + { + var dvProfile = state.VideoStream.DvProfile; + var dvLevel = state.VideoStream.DvLevel; + var dvRangeString = state.VideoStream.VideoRangeType switch + { + VideoRangeType.DOVIWithHDR10 => "db1p", + VideoRangeType.DOVIWithHLG => "db4h", + VideoRangeType.DOVIWithHDR10Plus => "db1p", // The HDR10+ metadata would be removed if Dovi metadata is not removed + _ => string.Empty // Don't label Dovi with EL and SDR due to compatability issues, ignore invalid configurations + }; + + if (dvProfile is null || dvLevel is null || string.IsNullOrEmpty(dvRangeString)) + { + return; + } + + var dvFourCc = string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase) ? "dav1" : "dvh1"; + builder.Append(",SUPPLEMENTAL-CODECS=\"") + .Append(dvFourCc) + .Append('.') + .Append(dvProfile.Value.ToString("D2", CultureInfo.InvariantCulture)) + .Append('.') + .Append(dvLevel.Value.ToString("D2", CultureInfo.InvariantCulture)) + .Append('/') + .Append(dvRangeString) + .Append('"'); + } + + void AppendHdr10PlusString() + { + var videoCodecLevel = GetOutputVideoCodecLevel(state); + if (string.IsNullOrEmpty(state.ActualOutputVideoCodec) || videoCodecLevel is null) + { + return; + } + + var videoCodecString = GetPlaylistVideoCodecs(state, state.ActualOutputVideoCodec, videoCodecLevel.Value); + builder.Append(",SUPPLEMENTAL-CODECS=\"") + .Append(videoCodecString) + .Append('/') + .Append("cdm4") + .Append('"'); + } } /// diff --git a/Jellyfin.Data/Enums/VideoRangeType.cs b/Jellyfin.Data/Enums/VideoRangeType.cs index 853c2c73db..ce232d73c3 100644 --- a/Jellyfin.Data/Enums/VideoRangeType.cs +++ b/Jellyfin.Data/Enums/VideoRangeType.cs @@ -45,6 +45,27 @@ public enum VideoRangeType /// DOVIWithSDR, + /// + /// Dolby Vision with Enhancment Layer (Profile 7). + /// + DOVIWithEL, + + /// + /// Dolby Vision and HDR10+ Metadata coexists. + /// + DOVIWithHDR10Plus, + + /// + /// Dolby Vision with Enhancment Layer (Profile 7) and HDR10+ Metadata coexists. + /// + DOVIWithELHDR10Plus, + + /// + /// Dolby Vision with invalid configuration. e.g. Profile 8 compat id 6. + /// When using this range, the server would assume the video is still HDR10 after removing the Dolby Vision metadata. + /// + DOVIInvalid, + /// /// HDR10+ video range type (10bit to 16bit). /// diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs index 36c3b9e565..1be31db72b 100644 --- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs @@ -140,6 +140,7 @@ public class MediaStreamRepository : IMediaStreamRepository dto.DvBlSignalCompatibilityId = entity.DvBlSignalCompatibilityId; dto.IsHearingImpaired = entity.IsHearingImpaired.GetValueOrDefault(); dto.Rotation = entity.Rotation; + dto.Hdr10PlusPresentFlag = entity.Hdr10PlusPresentFlag; if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle) { @@ -207,7 +208,8 @@ public class MediaStreamRepository : IMediaStreamRepository BlPresentFlag = dto.BlPresentFlag, DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId, IsHearingImpaired = dto.IsHearingImpaired, - Rotation = dto.Rotation + Rotation = dto.Rotation, + Hdr10PlusPresentFlag = dto.Hdr10PlusPresentFlag, }; return entity; } diff --git a/MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs new file mode 100644 index 0000000000..41d21e4404 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/BitStreamFilterOptionType.cs @@ -0,0 +1,32 @@ +namespace MediaBrowser.Controller.MediaEncoding; + +/// +/// Enum BitStreamFilterOptionType. +/// +public enum BitStreamFilterOptionType +{ + /// + /// hevc_metadata bsf with remove_dovi option. + /// + HevcMetadataRemoveDovi = 0, + + /// + /// hevc_metadata bsf with remove_hdr10plus option. + /// + HevcMetadataRemoveHdr10Plus = 1, + + /// + /// av1_metadata bsf with remove_dovi option. + /// + Av1MetadataRemoveDovi = 2, + + /// + /// av1_metadata bsf with remove_hdr10plus option. + /// + Av1MetadataRemoveHdr10Plus = 3, + + /// + /// dovi_rpu bsf with strip option. + /// + DoviRpuStrip = 4, +} diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index ed975af7fb..afa962a41c 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -162,6 +162,13 @@ namespace MediaBrowser.Controller.MediaEncoding _configurationManager = configurationManager; } + private enum DynamicHdrMetadataRemovalPlan + { + None, + RemoveDovi, + RemoveHdr10Plus, + } + [GeneratedRegex(@"\s+")] private static partial Regex WhiteSpaceRegex(); @@ -342,11 +349,8 @@ namespace MediaBrowser.Controller.MediaEncoding return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder || isVideoToolBoxDecoder; } - return state.VideoStream.VideoRange == VideoRange.HDR - && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10 - || state.VideoStream.VideoRangeType == VideoRangeType.HLG - || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10 - || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG); + // GPU tonemapping supports all HDR RangeTypes + return state.VideoStream.VideoRange == VideoRange.HDR; } private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) @@ -381,8 +385,7 @@ namespace MediaBrowser.Controller.MediaEncoding } return state.VideoStream.VideoRange == VideoRange.HDR - && (state.VideoStream.VideoRangeType == VideoRangeType.HDR10 - || state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10); + && IsDoviWithHdr10Bl(state.VideoStream); } private bool IsVideoToolboxTonemapAvailable(EncodingJobInfo state, EncodingOptions options) @@ -397,7 +400,8 @@ namespace MediaBrowser.Controller.MediaEncoding // Certain DV profile 5 video works in Safari with direct playing, but the VideoToolBox does not produce correct mapping results with transcoding. // All other HDR formats working. return state.VideoStream.VideoRange == VideoRange.HDR - && state.VideoStream.VideoRangeType is VideoRangeType.HDR10 or VideoRangeType.HLG or VideoRangeType.HDR10Plus or VideoRangeType.DOVIWithHDR10 or VideoRangeType.DOVIWithHLG; + && (IsDoviWithHdr10Bl(state.VideoStream) + || state.VideoStream.VideoRangeType is VideoRangeType.HLG); } private bool IsVideoStreamHevcRext(EncodingJobInfo state) @@ -1301,6 +1305,13 @@ namespace MediaBrowser.Controller.MediaEncoding || codec.Contains("hevc", StringComparison.OrdinalIgnoreCase); } + public static bool IsAv1(MediaStream stream) + { + var codec = stream.Codec ?? string.Empty; + + return codec.Contains("av1", StringComparison.OrdinalIgnoreCase); + } + public static bool IsAAC(MediaStream stream) { var codec = stream.Codec ?? string.Empty; @@ -1308,8 +1319,125 @@ namespace MediaBrowser.Controller.MediaEncoding return codec.Contains("aac", StringComparison.OrdinalIgnoreCase); } - public static string GetBitStreamArgs(MediaStream stream) + public static bool IsDoviWithHdr10Bl(MediaStream stream) { + var rangeType = stream?.VideoRangeType; + + return rangeType is VideoRangeType.DOVIWithHDR10 + or VideoRangeType.DOVIWithEL + or VideoRangeType.DOVIWithHDR10Plus + or VideoRangeType.DOVIWithELHDR10Plus + or VideoRangeType.DOVIInvalid; + } + + public static bool IsDovi(MediaStream stream) + { + var rangeType = stream?.VideoRangeType; + + return IsDoviWithHdr10Bl(stream) + || (rangeType is VideoRangeType.DOVI + or VideoRangeType.DOVIWithHLG + or VideoRangeType.DOVIWithSDR); + } + + public static bool IsHdr10Plus(MediaStream stream) + { + var rangeType = stream?.VideoRangeType; + + return rangeType is VideoRangeType.HDR10Plus + or VideoRangeType.DOVIWithHDR10Plus + or VideoRangeType.DOVIWithELHDR10Plus; + } + + /// + /// Check if dynamic HDR metadata should be removed during stream copy. + /// Please note this check assumes the range check has already been done + /// and trivial fallbacks like HDR10+ to HDR10, DOVIWithHDR10 to HDR10 is already checked. + /// + private static DynamicHdrMetadataRemovalPlan ShouldRemoveDynamicHdrMetadata(EncodingJobInfo state) + { + var videoStream = state.VideoStream; + if (videoStream.VideoRange is not VideoRange.HDR) + { + return DynamicHdrMetadataRemovalPlan.None; + } + + var requestedRangeTypes = state.GetRequestedRangeTypes(state.VideoStream.Codec); + if (requestedRangeTypes.Length == 0) + { + return DynamicHdrMetadataRemovalPlan.None; + } + + var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase); + var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase); + var requestHasDOVIwithEL = requestedRangeTypes.Contains(VideoRangeType.DOVIWithEL.ToString(), StringComparison.OrdinalIgnoreCase); + var requestHasDOVIwithELHDR10plus = requestedRangeTypes.Contains(VideoRangeType.DOVIWithELHDR10Plus.ToString(), StringComparison.OrdinalIgnoreCase); + + var shouldRemoveHdr10Plus = false; + // Case 1: Client supports HDR10, does not support DOVI with EL but EL presets + var shouldRemoveDovi = (!requestHasDOVIwithEL && requestHasHDR10) && videoStream.VideoRangeType == VideoRangeType.DOVIWithEL; + + // Case 2: Client supports DOVI, does not support broken DOVI config + // Client does not report DOVI support should be allowed to copy bad data for remuxing as HDR10 players would not crash + shouldRemoveDovi = shouldRemoveDovi || (requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVIInvalid); + + // Special case: we have a video with both EL and HDR10+ + // If the client supports EL but not in the case of coexistence with HDR10+, remove HDR10+ for compatibility reasons. + // Otherwise, remove DOVI if the client is not a DOVI player + if (videoStream.VideoRangeType == VideoRangeType.DOVIWithELHDR10Plus) + { + shouldRemoveHdr10Plus = requestHasDOVIwithEL && !requestHasDOVIwithELHDR10plus; + shouldRemoveDovi = shouldRemoveDovi || !shouldRemoveHdr10Plus; + } + + if (shouldRemoveDovi) + { + return DynamicHdrMetadataRemovalPlan.RemoveDovi; + } + + // If the client is a Dolby Vision Player, remove the HDR10+ metadata to avoid playback issues + shouldRemoveHdr10Plus = shouldRemoveHdr10Plus || (requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10Plus); + return shouldRemoveHdr10Plus ? DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus : DynamicHdrMetadataRemovalPlan.None; + } + + private bool CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan plan, MediaStream videoStream) + { + return plan switch + { + DynamicHdrMetadataRemovalPlan.RemoveDovi => _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.DoviRpuStrip) + || (IsH265(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveDovi)) + || (IsAv1(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveDovi)), + DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus => (IsH265(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveHdr10Plus)) + || (IsAv1(videoStream) && _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveHdr10Plus)), + _ => true, + }; + } + + public bool IsDoviRemoved(EncodingJobInfo state) + { + return state?.VideoStream is not null && ShouldRemoveDynamicHdrMetadata(state) == DynamicHdrMetadataRemovalPlan.RemoveDovi + && CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan.RemoveDovi, state.VideoStream); + } + + public bool IsHdr10PlusRemoved(EncodingJobInfo state) + { + return state?.VideoStream is not null && ShouldRemoveDynamicHdrMetadata(state) == DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus + && CanEncoderRemoveDynamicHdrMetadata(DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus, state.VideoStream); + } + + public string GetBitStreamArgs(EncodingJobInfo state, MediaStreamType streamType) + { + if (state is null) + { + return null; + } + + var stream = streamType switch + { + MediaStreamType.Audio => state.AudioStream, + MediaStreamType.Video => state.VideoStream, + _ => state.VideoStream + }; // TODO This is auto inserted into the mpegts mux so it might not be needed. // https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb if (IsH264(stream)) @@ -1317,21 +1445,57 @@ namespace MediaBrowser.Controller.MediaEncoding return "-bsf:v h264_mp4toannexb"; } - if (IsH265(stream)) - { - return "-bsf:v hevc_mp4toannexb"; - } - if (IsAAC(stream)) { // Convert adts header(mpegts) to asc header(mp4). return "-bsf:a aac_adtstoasc"; } + if (IsH265(stream)) + { + var filter = "-bsf:v hevc_mp4toannexb"; + + // The following checks are not complete because the copy would be rejected + // if the encoder cannot remove required metadata. + // And if bsf is used, we must already be using copy codec. + switch (ShouldRemoveDynamicHdrMetadata(state)) + { + default: + case DynamicHdrMetadataRemovalPlan.None: + break; + case DynamicHdrMetadataRemovalPlan.RemoveDovi: + filter += _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.HevcMetadataRemoveDovi) + ? ",hevc_metadata=remove_dovi=1" + : ",dovi_rpu=strip=1"; + break; + case DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus: + filter += ",hevc_metadata=remove_hdr10plus=1"; + break; + } + + return filter; + } + + if (IsAv1(stream)) + { + switch (ShouldRemoveDynamicHdrMetadata(state)) + { + default: + case DynamicHdrMetadataRemovalPlan.None: + return null; + case DynamicHdrMetadataRemovalPlan.RemoveDovi: + return _mediaEncoder.SupportsBitStreamFilterWithOption(BitStreamFilterOptionType.Av1MetadataRemoveDovi) + ? "-bsf:v av1_metadata=remove_dovi=1" + : "-bsf:v dovi_rpu=strip=1"; + case DynamicHdrMetadataRemovalPlan.RemoveHdr10Plus: + return "-bsf:v av1_metadata=remove_hdr10plus=1"; + } + } + return null; } - public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer) + public string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer) { var bitStreamArgs = string.Empty; var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.'); @@ -1342,7 +1506,7 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals(mediaSourceContainer, "aac", StringComparison.OrdinalIgnoreCase) || string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase))) { - bitStreamArgs = GetBitStreamArgs(state.AudioStream); + bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Audio); bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs; } @@ -2169,7 +2333,6 @@ namespace MediaBrowser.Controller.MediaEncoding } // DOVIWithHDR10 should be compatible with HDR10 supporting players. Same goes with HLG and of course SDR. So allow copy of those formats - var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase); var requestHasHLG = requestedRangeTypes.Contains(VideoRangeType.HLG.ToString(), StringComparison.OrdinalIgnoreCase); var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase); @@ -2177,9 +2340,17 @@ namespace MediaBrowser.Controller.MediaEncoding if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase) && !((requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10) || (requestHasHLG && videoStream.VideoRangeType == VideoRangeType.DOVIWithHLG) - || (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR))) + || (requestHasSDR && videoStream.VideoRangeType == VideoRangeType.DOVIWithSDR) + || (requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.HDR10Plus))) { - return false; + // Check complicated cases where we need to remove dynamic metadata + // Conservatively refuse to copy if the encoder can't remove dynamic metadata, + // but a removal is required for compatability reasons. + var dynamicHdrMetadataRemovalPlan = ShouldRemoveDynamicHdrMetadata(state); + if (!CanEncoderRemoveDynamicHdrMetadata(dynamicHdrMetadataRemovalPlan, videoStream)) + { + return false; + } } } @@ -7244,7 +7415,7 @@ namespace MediaBrowser.Controller.MediaEncoding && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { - string bitStreamArgs = GetBitStreamArgs(state.VideoStream); + string bitStreamArgs = GetBitStreamArgs(state, MediaStreamType.Video); if (!string.IsNullOrEmpty(bitStreamArgs)) { args += " " + bitStreamArgs; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 7586ac9024..8d6211051b 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Linq; using Jellyfin.Data.Enums; diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index a60f523408..de6353c4c1 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -116,6 +116,13 @@ namespace MediaBrowser.Controller.MediaEncoding /// true if the filter is supported, false otherwise. bool SupportsFilterWithOption(FilterOptionType option); + /// + /// Whether the bitstream filter is supported with the given option. + /// + /// The option. + /// true if the bitstream filter is supported, false otherwise. + bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option); + /// /// Extracts the audio image. /// diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 54d0eb4b51..d28cd70ef5 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Linq; using System.Runtime.Versioning; using System.Text.RegularExpressions; +using MediaBrowser.Controller.MediaEncoding; using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Encoder @@ -160,6 +161,15 @@ namespace MediaBrowser.MediaEncoding.Encoder { 6, new string[] { "transpose_opencl", "rotate by half-turn" } } }; + private static readonly Dictionary _bsfOptionsDict = new Dictionary + { + { BitStreamFilterOptionType.HevcMetadataRemoveDovi, ("hevc_metadata", "remove_dovi") }, + { BitStreamFilterOptionType.HevcMetadataRemoveHdr10Plus, ("hevc_metadata", "remove_hdr10plus") }, + { BitStreamFilterOptionType.Av1MetadataRemoveDovi, ("av1_metadata", "remove_dovi") }, + { BitStreamFilterOptionType.Av1MetadataRemoveHdr10Plus, ("av1_metadata", "remove_hdr10plus") }, + { BitStreamFilterOptionType.DoviRpuStrip, ("dovi_rpu", "strip") } + }; + // These are the library versions that corresponds to our minimum ffmpeg version 4.4 according to the version table below // Refers to the versions in https://ffmpeg.org/download.html private static readonly Dictionary _ffmpegMinimumLibraryVersions = new Dictionary @@ -286,6 +296,9 @@ namespace MediaBrowser.MediaEncoding.Encoder public IDictionary GetFiltersWithOption() => GetFFmpegFiltersWithOption(); + public IDictionary GetBitStreamFiltersWithOption() => _bsfOptionsDict + .ToDictionary(item => item.Key, item => CheckBitStreamFilterWithOption(item.Value.Item1, item.Value.Item2)); + public Version? GetFFmpegVersion() { string output; @@ -495,6 +508,34 @@ namespace MediaBrowser.MediaEncoding.Encoder return false; } + public bool CheckBitStreamFilterWithOption(string filter, string option) + { + if (string.IsNullOrEmpty(filter) || string.IsNullOrEmpty(option)) + { + return false; + } + + string output; + try + { + output = GetProcessOutput(_encoderPath, "-h bsf=" + filter, false, null); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error detecting the given bit stream filter"); + return false; + } + + if (output.Contains("Bit stream filter " + filter, StringComparison.Ordinal)) + { + return output.Contains(option, StringComparison.Ordinal); + } + + _logger.LogWarning("Bit stream filter: {Name} with option {Option} is not available", filter, option); + + return false; + } + public bool CheckSupportedRuntimeKey(string keyDesc, Version? ffmpegVersion) { if (string.IsNullOrEmpty(keyDesc)) @@ -523,6 +564,11 @@ namespace MediaBrowser.MediaEncoding.Encoder return !string.IsNullOrEmpty(flag) && GetProcessExitCode(_encoderPath, $"-loglevel quiet -hwaccel_flags +{flag} -hide_banner -f lavfi -i nullsrc=s=1x1:d=100 -f null -"); } + public bool CheckSupportedProberOption(string option, string proberPath) + { + return !string.IsNullOrEmpty(option) && GetProcessExitCode(proberPath, $"-loglevel quiet -f lavfi -i nullsrc=s=1x1:d=1 -{option}"); + } + private IEnumerable GetCodecs(Codec codec) { string codecstr = codec == Codec.Encoder ? "encoders" : "decoders"; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index e96040506f..9a759ba418 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -73,9 +73,11 @@ namespace MediaBrowser.MediaEncoding.Encoder private List _hwaccels = new List(); private List _filters = new List(); private IDictionary _filtersWithOption = new Dictionary(); + private IDictionary _bitStreamFiltersWithOption = new Dictionary(); private bool _isPkeyPauseSupported = false; private bool _isLowPriorityHwDecodeSupported = false; + private bool _proberSupportsFirstVideoFrame = false; private bool _isVaapiDeviceAmd = false; private bool _isVaapiDeviceInteliHD = false; @@ -222,6 +224,7 @@ namespace MediaBrowser.MediaEncoding.Encoder SetAvailableEncoders(validator.GetEncoders()); SetAvailableFilters(validator.GetFilters()); SetAvailableFiltersWithOption(validator.GetFiltersWithOption()); + SetAvailableBitStreamFiltersWithOption(validator.GetBitStreamFiltersWithOption()); SetAvailableHwaccels(validator.GetHwaccels()); SetMediaEncoderVersion(validator); @@ -229,6 +232,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _isPkeyPauseSupported = validator.CheckSupportedRuntimeKey("p pause transcoding", _ffmpegVersion); _isLowPriorityHwDecodeSupported = validator.CheckSupportedHwaccelFlag("low_priority"); + _proberSupportsFirstVideoFrame = validator.CheckSupportedProberOption("only_first_vframe", _ffprobePath); // Check the Vaapi device vendor if (OperatingSystem.IsLinux() @@ -342,6 +346,11 @@ namespace MediaBrowser.MediaEncoding.Encoder _filtersWithOption = dict; } + public void SetAvailableBitStreamFiltersWithOption(IDictionary dict) + { + _bitStreamFiltersWithOption = dict; + } + public void SetMediaEncoderVersion(EncoderValidator validator) { _ffmpegVersion = validator.GetFFmpegVersion(); @@ -382,6 +391,11 @@ namespace MediaBrowser.MediaEncoding.Encoder return false; } + public bool SupportsBitStreamFilterWithOption(BitStreamFilterOptionType option) + { + return _bitStreamFiltersWithOption.TryGetValue(option, out var val) && val; + } + public bool CanEncodeToAudioCodec(string codec) { if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase)) @@ -501,6 +515,12 @@ namespace MediaBrowser.MediaEncoding.Encoder var args = extractChapters ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format" : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format"; + + if (_proberSupportsFirstVideoFrame) + { + args += " -show_frames -only_first_vframe"; + } + args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, _threads).Trim(); var process = new Process diff --git a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs index d4d153b083..53eea64db1 100644 --- a/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs +++ b/MediaBrowser.MediaEncoding/Probing/InternalMediaInfoResult.cs @@ -30,5 +30,12 @@ namespace MediaBrowser.MediaEncoding.Probing /// The chapters. [JsonPropertyName("chapters")] public IReadOnlyList Chapters { get; set; } + + /// + /// Gets or sets the frames. + /// + /// The streams. + [JsonPropertyName("frames")] + public IReadOnlyList Frames { get; set; } } } diff --git a/MediaBrowser.MediaEncoding/Probing/MediaFrameInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaFrameInfo.cs new file mode 100644 index 0000000000..bed4368ed2 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Probing/MediaFrameInfo.cs @@ -0,0 +1,184 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace MediaBrowser.MediaEncoding.Probing; + +/// +/// Class MediaFrameInfo. +/// +public class MediaFrameInfo +{ + /// + /// Gets or sets the media type. + /// + [JsonPropertyName("media_type")] + public string? MediaType { get; set; } + + /// + /// Gets or sets the StreamIndex. + /// + [JsonPropertyName("stream_index")] + public int? StreamIndex { get; set; } + + /// + /// Gets or sets the KeyFrame. + /// + [JsonPropertyName("key_frame")] + public int? KeyFrame { get; set; } + + /// + /// Gets or sets the Pts. + /// + [JsonPropertyName("pts")] + public long? Pts { get; set; } + + /// + /// Gets or sets the PtsTime. + /// + [JsonPropertyName("pts_time")] + public string? PtsTime { get; set; } + + /// + /// Gets or sets the BestEffortTimestamp. + /// + [JsonPropertyName("best_effort_timestamp")] + public long BestEffortTimestamp { get; set; } + + /// + /// Gets or sets the BestEffortTimestampTime. + /// + [JsonPropertyName("best_effort_timestamp_time")] + public string? BestEffortTimestampTime { get; set; } + + /// + /// Gets or sets the Duration. + /// + [JsonPropertyName("duration")] + public int Duration { get; set; } + + /// + /// Gets or sets the DurationTime. + /// + [JsonPropertyName("duration_time")] + public string? DurationTime { get; set; } + + /// + /// Gets or sets the PktPos. + /// + [JsonPropertyName("pkt_pos")] + public string? PktPos { get; set; } + + /// + /// Gets or sets the PktSize. + /// + [JsonPropertyName("pkt_size")] + public string? PktSize { get; set; } + + /// + /// Gets or sets the Width. + /// + [JsonPropertyName("width")] + public int? Width { get; set; } + + /// + /// Gets or sets the Height. + /// + [JsonPropertyName("height")] + public int? Height { get; set; } + + /// + /// Gets or sets the CropTop. + /// + [JsonPropertyName("crop_top")] + public int? CropTop { get; set; } + + /// + /// Gets or sets the CropBottom. + /// + [JsonPropertyName("crop_bottom")] + public int? CropBottom { get; set; } + + /// + /// Gets or sets the CropLeft. + /// + [JsonPropertyName("crop_left")] + public int? CropLeft { get; set; } + + /// + /// Gets or sets the CropRight. + /// + [JsonPropertyName("crop_right")] + public int? CropRight { get; set; } + + /// + /// Gets or sets the PixFmt. + /// + [JsonPropertyName("pix_fmt")] + public string? PixFmt { get; set; } + + /// + /// Gets or sets the SampleAspectRatio. + /// + [JsonPropertyName("sample_aspect_ratio")] + public string? SampleAspectRatio { get; set; } + + /// + /// Gets or sets the PictType. + /// + [JsonPropertyName("pict_type")] + public string? PictType { get; set; } + + /// + /// Gets or sets the InterlacedFrame. + /// + [JsonPropertyName("interlaced_frame")] + public int? InterlacedFrame { get; set; } + + /// + /// Gets or sets the TopFieldFirst. + /// + [JsonPropertyName("top_field_first")] + public int? TopFieldFirst { get; set; } + + /// + /// Gets or sets the RepeatPict. + /// + [JsonPropertyName("repeat_pict")] + public int? RepeatPict { get; set; } + + /// + /// Gets or sets the ColorRange. + /// + [JsonPropertyName("color_range")] + public string? ColorRange { get; set; } + + /// + /// Gets or sets the ColorSpace. + /// + [JsonPropertyName("color_space")] + public string? ColorSpace { get; set; } + + /// + /// Gets or sets the ColorPrimaries. + /// + [JsonPropertyName("color_primaries")] + public string? ColorPrimaries { get; set; } + + /// + /// Gets or sets the ColorTransfer. + /// + [JsonPropertyName("color_transfer")] + public string? ColorTransfer { get; set; } + + /// + /// Gets or sets the ChromaLocation. + /// + [JsonPropertyName("chroma_location")] + public string? ChromaLocation { get; set; } + + /// + /// Gets or sets the SideDataList. + /// + [JsonPropertyName("side_data_list")] + public IReadOnlyList? SideDataList { get; set; } +} diff --git a/MediaBrowser.MediaEncoding/Probing/MediaFrameSideDataInfo.cs b/MediaBrowser.MediaEncoding/Probing/MediaFrameSideDataInfo.cs new file mode 100644 index 0000000000..3f7dd9a69d --- /dev/null +++ b/MediaBrowser.MediaEncoding/Probing/MediaFrameSideDataInfo.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace MediaBrowser.MediaEncoding.Probing; + +/// +/// Class MediaFrameSideDataInfo. +/// Currently only records the SideDataType for HDR10+ detection. +/// +public class MediaFrameSideDataInfo +{ + /// + /// Gets or sets the SideDataType. + /// + [JsonPropertyName("side_data_type")] + public string? SideDataType { get; set; } +} diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 6b0fd9a147..a98dbe5970 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -105,8 +105,9 @@ namespace MediaBrowser.MediaEncoding.Probing SetSize(data, info); var internalStreams = data.Streams ?? Array.Empty(); + var internalFrames = data.Frames ?? Array.Empty(); - info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format)) + info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format, internalFrames)) .Where(i => i is not null) // Drop subtitle streams if we don't know the codec because it will just cause failures if we don't know how to handle them .Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec)) @@ -685,8 +686,9 @@ namespace MediaBrowser.MediaEncoding.Probing /// if set to true [is info]. /// The stream info. /// The format info. + /// The frame info. /// MediaStream. - private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo) + private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo, IReadOnlyList frameInfoList) { // These are mp4 chapters if (string.Equals(streamInfo.CodecName, "mov_text", StringComparison.OrdinalIgnoreCase)) @@ -904,6 +906,15 @@ namespace MediaBrowser.MediaEncoding.Probing } } } + + var frameInfo = frameInfoList?.FirstOrDefault(i => i.StreamIndex == stream.Index); + if (frameInfo?.SideDataList != null) + { + if (frameInfo.SideDataList.Any(data => string.Equals(data.SideDataType, "HDR Dynamic Metadata SMPTE2094-40 (HDR10+)", StringComparison.OrdinalIgnoreCase))) + { + stream.Hdr10PlusPresentFlag = true; + } + } } else if (streamInfo.CodecType == CodecType.Data) { diff --git a/MediaBrowser.Model/Dlna/ConditionProcessor.cs b/MediaBrowser.Model/Dlna/ConditionProcessor.cs index 09b9663679..1b61bfe155 100644 --- a/MediaBrowser.Model/Dlna/ConditionProcessor.cs +++ b/MediaBrowser.Model/Dlna/ConditionProcessor.cs @@ -345,6 +345,15 @@ namespace MediaBrowser.Model.Dlna return !condition.IsRequired; } + // Special case: HDR10 also satisfies if the video is HDR10Plus + if (currentValue.Value == VideoRangeType.HDR10Plus) + { + if (IsConditionSatisfied(condition, VideoRangeType.HDR10)) + { + return true; + } + } + var conditionType = condition.Condition; if (conditionType == ProfileConditionType.EqualsAny) { diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index d89386c1ca..13acd15a3f 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Linq; using System.Text; diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index dae3d84ae6..95b5b43f87 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -153,6 +153,8 @@ namespace MediaBrowser.Model.Entities /// The title. public string Title { get; set; } + public bool? Hdr10PlusPresentFlag { get; set; } + /// /// Gets the video range. /// @@ -172,6 +174,7 @@ namespace MediaBrowser.Model.Entities /// Gets the video range type. /// /// The video range type. + [DefaultValue(VideoRangeType.Unknown)] public VideoRangeType VideoRangeType { get @@ -779,8 +782,8 @@ namespace MediaBrowser.Model.Entities var blPresentFlag = BlPresentFlag == 1; var dvBlCompatId = DvBlSignalCompatibilityId; - var isDoViProfile = dvProfile == 5 || dvProfile == 7 || dvProfile == 8 || dvProfile == 10; - var isDoViFlag = rpuPresentFlag && blPresentFlag && (dvBlCompatId == 0 || dvBlCompatId == 1 || dvBlCompatId == 4 || dvBlCompatId == 2 || dvBlCompatId == 6); + var isDoViProfile = dvProfile is 5 or 7 or 8 or 10; + var isDoViFlag = rpuPresentFlag && blPresentFlag && dvBlCompatId is 0 or 1 or 4 or 2 or 6; if ((isDoViProfile && isDoViFlag) || string.Equals(codecTag, "dovi", StringComparison.OrdinalIgnoreCase) @@ -788,7 +791,7 @@ namespace MediaBrowser.Model.Entities || string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase) || string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase)) { - return dvProfile switch + var dvRangeSet = dvProfile switch { 5 => (VideoRange.HDR, VideoRangeType.DOVI), 8 => dvBlCompatId switch @@ -796,32 +799,40 @@ namespace MediaBrowser.Model.Entities 1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), 4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG), 2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR), - // While not in Dolby Spec, Profile 8 CCid 6 media are possible to create, and since CCid 6 stems from Bluray (Profile 7 originally) an HDR10 base layer is guaranteed to exist. - 6 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), - // There is no other case to handle here as per Dolby Spec. Default case included for completeness and linting purposes - _ => (VideoRange.SDR, VideoRangeType.SDR) + // Out of Dolby Spec files should be marked as invalid + _ => (VideoRange.HDR, VideoRangeType.DOVIInvalid) }, - 7 => (VideoRange.HDR, VideoRangeType.HDR10), + 7 => (VideoRange.HDR, VideoRangeType.DOVIWithEL), 10 => dvBlCompatId switch { 0 => (VideoRange.HDR, VideoRangeType.DOVI), 1 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), 2 => (VideoRange.SDR, VideoRangeType.DOVIWithSDR), 4 => (VideoRange.HDR, VideoRangeType.DOVIWithHLG), - // While not in Dolby Spec, Profile 8 CCid 6 media are possible to create, and since CCid 6 stems from Bluray (Profile 7 originally) an HDR10 base layer is guaranteed to exist. - 6 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10), - // There is no other case to handle here as per Dolby Spec. Default case included for completeness and linting purposes - _ => (VideoRange.SDR, VideoRangeType.SDR) + // Out of Dolby Spec files should be marked as invalid + _ => (VideoRange.HDR, VideoRangeType.DOVIInvalid) }, _ => (VideoRange.SDR, VideoRangeType.SDR) }; + + if (Hdr10PlusPresentFlag == true) + { + return dvRangeSet.Item2 switch + { + VideoRangeType.DOVIWithHDR10 => (VideoRange.HDR, VideoRangeType.DOVIWithHDR10Plus), + VideoRangeType.DOVIWithEL => (VideoRange.HDR, VideoRangeType.DOVIWithELHDR10Plus), + _ => dvRangeSet + }; + } + + return dvRangeSet; } var colorTransfer = ColorTransfer; if (string.Equals(colorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)) { - return (VideoRange.HDR, VideoRangeType.HDR10); + return Hdr10PlusPresentFlag == true ? (VideoRange.HDR, VideoRangeType.HDR10Plus) : (VideoRange.HDR, VideoRangeType.HDR10); } else if (string.Equals(colorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs index 207317376d..b80b764ba3 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs @@ -99,4 +99,6 @@ public class MediaStreamInfo public int? Rotation { get; set; } public string? KeyFrames { get; set; } + + public bool? Hdr10PlusPresentFlag { get; set; } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.Designer.cs new file mode 100644 index 0000000000..bad01778da --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.Designer.cs @@ -0,0 +1,1655 @@ +// +using System; +using Jellyfin.Database.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20250327171413_AddHdr10PlusFlag")] + partial class AddHdr10PlusFlag + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue") + .IsUnique(); + + b.ToTable("ItemValues"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Hdr10PlusPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "UserId", "CustomDataKey"); + + b.HasIndex("UserId"); + + b.HasIndex("ItemId", "UserId", "IsFavorite"); + + b.HasIndex("ItemId", "UserId", "LastPlayedDate"); + + b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("ItemId", "UserId", "Played"); + + b.ToTable("UserData"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Children") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem") + .WithMany("ParentAncestors") + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("UserData") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Navigation("Chapters"); + + b.Navigation("Children"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("ParentAncestors"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs new file mode 100644 index 0000000000..5766cd3825 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class AddHdr10PlusFlag : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Hdr10PlusPresentFlag", + table: "MediaStreamInfos", + type: "INTEGER", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Hdr10PlusPresentFlag", + table: "MediaStreamInfos"); + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs index 8b2b26934579898be33a43311ef6a83ac8a97ccf..adc15684fddc5b0d30482a0326493f70519da3e8 100644 GIT binary patch delta 32 ocmex*iuw6z<_!&nlN01wcplsr<33=INuN{ctk7lxGp0Ni5?GXMYp delta 22 ecmaEUn)&M~<_!&nlNZP)Pi`pW*c@BbRSW=zZ3@=_ From 0573999d5ef7526a3bb3e24523ba0e5599816155 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Thu, 3 Apr 2025 02:06:40 +0200 Subject: [PATCH 165/508] Import Keyframes into database (#13771) * Migrate keyframe data into database * Clear database table before import to handle failed migrations --- .../ApplicationHost.cs | 1 + .../Controllers/DynamicHlsController.cs | 1 + .../Item/BaseItemRepository.cs | 1 + .../Item/KeyframeRepository.cs | 64 + Jellyfin.Server/Migrations/MigrationRunner.cs | 1 + .../Routines/MigrateKeyframeData.cs | 173 ++ .../MediaBrowser.Controller.csproj | 1 + .../Persistence/IKeyframeRepository.cs | 29 + .../Entities/KeyframeData.cs | 32 + .../JellyfinDbContext.cs | 5 + .../KeyframeDataConfiguration.cs | 18 + ...20250327101120_AddKeyframeData.Designer.cs | 1681 +++++++++++++++++ .../20250327101120_AddKeyframeData.cs | 41 + .../Migrations/JellyfinDbModelSnapshot.cs | Bin 58855 -> 59931 bytes .../Cache/CacheDecorator.cs | 79 +- .../Extractors/FfProbeKeyframeExtractor.cs | 2 +- .../Extractors/IKeyframeExtractor.cs | 4 +- .../Extractors/MatroskaKeyframeExtractor.cs | 2 +- .../Jellyfin.MediaEncoding.Hls.csproj | 1 + .../Playlist/CreateMainPlaylistRequest.cs | 11 +- .../Playlist/DynamicHlsPlaylistGenerator.cs | 6 +- .../KeyframeExtractionScheduledTask.cs | 24 +- 22 files changed, 2099 insertions(+), 78 deletions(-) create mode 100644 Jellyfin.Server.Implementations/Item/KeyframeRepository.cs create mode 100644 Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs create mode 100644 MediaBrowser.Controller/Persistence/IKeyframeRepository.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/KeyframeData.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/KeyframeDataConfiguration.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.Designer.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 4d959905d9..5bb75e2b95 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -505,6 +505,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 31b96972ea..b501bae4ca 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1421,6 +1421,7 @@ public class DynamicHlsController : BaseJellyfinApiController .ConfigureAwait(false); var request = new CreateMainPlaylistRequest( + Guid.Parse(state.BaseRequest.MediaSourceId), state.MediaPath, state.SegmentLength * 1000, state.RunTimeTicks ?? 0, diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 08c024f437..c7cd54ed18 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -114,6 +114,7 @@ public sealed class BaseItemRepository context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete(); context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete(); context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete(); + context.KeyframeData.Where(e => e.ItemId == id).ExecuteDelete(); context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete(); context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete(); diff --git a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs new file mode 100644 index 0000000000..a2267700fb --- /dev/null +++ b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; +using MediaBrowser.Controller.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Server.Implementations.Item; + +/// +/// Repository for obtaining Keyframe data. +/// +public class KeyframeRepository : IKeyframeRepository +{ + private readonly IDbContextFactory _dbProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The EFCore db factory. + public KeyframeRepository(IDbContextFactory dbProvider) + { + _dbProvider = dbProvider; + } + + private static MediaEncoding.Keyframes.KeyframeData Map(KeyframeData entity) + { + return new MediaEncoding.Keyframes.KeyframeData( + entity.TotalDuration, + (entity.KeyframeTicks ?? []).ToList()); + } + + private KeyframeData Map(MediaEncoding.Keyframes.KeyframeData dto, Guid itemId) + { + return new() + { + ItemId = itemId, + TotalDuration = dto.TotalDuration, + KeyframeTicks = dto.KeyframeTicks.ToList() + }; + } + + /// + public IReadOnlyList GetKeyframeData(Guid itemId) + { + using var context = _dbProvider.CreateDbContext(); + + return context.KeyframeData.AsNoTracking().Where(e => e.ItemId.Equals(itemId)).Select(e => Map(e)).ToList(); + } + + /// + public async Task SaveKeyframeDataAsync(Guid itemId, MediaEncoding.Keyframes.KeyframeData data, CancellationToken cancellationToken) + { + using var context = _dbProvider.CreateDbContext(); + using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false); + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + } +} diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 68a3491b57..baeea2c14a 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -56,6 +56,7 @@ namespace Jellyfin.Server.Migrations typeof(Routines.MigrateLibraryDb), typeof(Routines.MigrateRatingLevels), typeof(Routines.MoveTrickplayFiles), + typeof(Routines.MigrateKeyframeData), }; /// diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs new file mode 100644 index 0000000000..1cab943c1a --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.Json; +using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Extensions.Json; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +/// +/// Migration to move extracted files to the new directories. +/// +public class MigrateKeyframeData : IDatabaseMigrationRoutine +{ + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IApplicationPaths _appPaths; + private readonly IDbContextFactory _dbProvider; + private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// The logger. + /// Instance of the interface. + /// The EFCore db factory. + public MigrateKeyframeData( + ILibraryManager libraryManager, + ILogger logger, + IApplicationPaths appPaths, + IDbContextFactory dbProvider) + { + _libraryManager = libraryManager; + _logger = logger; + _appPaths = appPaths; + _dbProvider = dbProvider; + } + + private string KeyframeCachePath => Path.Combine(_appPaths.DataPath, "keyframes"); + + /// + public Guid Id => new("EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24"); + + /// + public string Name => "MigrateKeyframeData"; + + /// + public bool PerformOnNewInstall => false; + + /// + public void Perform() + { + const int Limit = 100; + int itemCount = 0, offset = 0, previousCount; + + var sw = Stopwatch.StartNew(); + var itemsQuery = new InternalItemsQuery + { + MediaTypes = [MediaType.Video], + SourceTypes = [SourceType.Library], + IsVirtualItem = false, + IsFolder = false + }; + + using var context = _dbProvider.CreateDbContext(); + context.KeyframeData.ExecuteDelete(); + using var transaction = context.Database.BeginTransaction(); + List keyframes = []; + + do + { + var result = _libraryManager.GetItemsResult(itemsQuery); + _logger.LogInformation("Importing keyframes for {Count} items", result.TotalRecordCount); + + var items = result.Items; + previousCount = items.Count; + offset += Limit; + foreach (var item in items) + { + if (TryGetKeyframeData(item, out var data)) + { + keyframes.Add(data); + } + + if (++itemCount % 10_000 == 0) + { + context.KeyframeData.AddRange(keyframes); + keyframes.Clear(); + _logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed); + } + } + } while (previousCount == Limit); + + context.KeyframeData.AddRange(keyframes); + context.SaveChanges(); + transaction.Commit(); + + _logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed); + + Directory.Delete(KeyframeCachePath, true); + } + + private bool TryGetKeyframeData(BaseItem item, [NotNullWhen(true)] out KeyframeData? data) + { + data = null; + var path = item.Path; + if (!string.IsNullOrEmpty(path)) + { + var cachePath = GetCachePath(KeyframeCachePath, path); + if (TryReadFromCache(cachePath, out var keyframeData)) + { + data = new() + { + ItemId = item.Id, + KeyframeTicks = keyframeData.KeyframeTicks.ToList(), + TotalDuration = keyframeData.TotalDuration + }; + + return true; + } + } + + return false; + } + + private string? GetCachePath(string keyframeCachePath, string filePath) + { + DateTime? lastWriteTimeUtc; + try + { + lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath); + } + catch (IOException e) + { + _logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message); + + return null; + } + + ReadOnlySpan filename = (filePath + "_" + lastWriteTimeUtc.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5() + ".json"; + var prefix = filename[..1]; + + return Path.Join(keyframeCachePath, prefix, filename); + } + + private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult) + { + if (File.Exists(cachePath)) + { + var bytes = File.ReadAllBytes(cachePath); + cachedResult = JsonSerializer.Deserialize(bytes, _jsonOptions); + + return cachedResult is not null; + } + + cachedResult = null; + + return false; + } +} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index d8aaf5ba01..3353ad63f1 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -28,6 +28,7 @@ + diff --git a/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs new file mode 100644 index 0000000000..4930434a7f --- /dev/null +++ b/MediaBrowser.Controller/Persistence/IKeyframeRepository.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.MediaEncoding.Keyframes; + +namespace MediaBrowser.Controller.Persistence; + +/// +/// Provides methods for accessing keyframe data. +/// +public interface IKeyframeRepository +{ + /// + /// Gets the keyframe data. + /// + /// The item id. + /// The keyframe data. + IReadOnlyList GetKeyframeData(Guid itemId); + + /// + /// Saves the keyframe data. + /// + /// The item id. + /// The keyframe data. + /// The cancellation token. + /// The task object representing the asynchronous operation. + Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken); +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/KeyframeData.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/KeyframeData.cs new file mode 100644 index 0000000000..c34110c4f2 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/KeyframeData.cs @@ -0,0 +1,32 @@ +#pragma warning disable CA2227 // Collection properties should be read only + +using System; +using System.Collections.Generic; + +namespace Jellyfin.Database.Implementations.Entities; + +/// +/// Keyframe information for a specific file. +/// +public class KeyframeData +{ + /// + /// Gets or Sets the ItemId. + /// + public required Guid ItemId { get; set; } + + /// + /// Gets or sets the total duration of the stream in ticks. + /// + public long TotalDuration { get; set; } + + /// + /// Gets or sets the keyframes in ticks. + /// + public ICollection? KeyframeTicks { get; set; } + + /// + /// Gets or sets the item reference. + /// + public BaseItemEntity? Item { get; set; } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs index 9db70263d2..35ad461ec7 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs @@ -157,6 +157,11 @@ public class JellyfinDbContext(DbContextOptions options, ILog /// public DbSet BaseItemTrailerTypes => Set(); + /// + /// Gets the . + /// + public DbSet KeyframeData => Set(); + /*public DbSet Artwork => Set(); public DbSet Books => Set(); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/KeyframeDataConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/KeyframeDataConfiguration.cs new file mode 100644 index 0000000000..3f5d458ca2 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/KeyframeDataConfiguration.cs @@ -0,0 +1,18 @@ +using Jellyfin.Database.Implementations.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Jellyfin.Database.Implementations.ModelConfiguration; + +/// +/// KeyframeData Configuration. +/// +public class KeyframeDataConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => e.ItemId); + builder.HasOne(e => e.Item).WithMany().HasForeignKey(e => e.ItemId); + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.Designer.cs new file mode 100644 index 0000000000..434ea820af --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.Designer.cs @@ -0,0 +1,1681 @@ +// +using System; +using Jellyfin.Database.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20250327101120_AddKeyframeData")] + partial class AddKeyframeData + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue") + .IsUnique(); + + b.ToTable("ItemValues"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("KeyframeTicks") + .HasColumnType("TEXT"); + + b.Property("TotalDuration") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId"); + + b.ToTable("KeyframeData"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "UserId", "CustomDataKey"); + + b.HasIndex("UserId"); + + b.HasIndex("ItemId", "UserId", "IsFavorite"); + + b.HasIndex("ItemId", "UserId", "LastPlayedDate"); + + b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("ItemId", "UserId", "Played"); + + b.ToTable("UserData"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Children") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem") + .WithMany("ParentAncestors") + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("UserData") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Navigation("Chapters"); + + b.Navigation("Children"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("ParentAncestors"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs new file mode 100644 index 0000000000..c17b35b404 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class AddKeyframeData : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "KeyframeData", + columns: table => new + { + ItemId = table.Column(type: "TEXT", nullable: false), + TotalDuration = table.Column(type: "INTEGER", nullable: false), + KeyframeTicks = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_KeyframeData", x => x.ItemId); + table.ForeignKey( + name: "FK_KeyframeData_BaseItems_ItemId", + column: x => x.ItemId, + principalTable: "BaseItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "KeyframeData"); + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs index adc15684fddc5b0d30482a0326493f70519da3e8..0bb4b31b051ac185eca02c29578e437c3e09ff66 100644 GIT binary patch delta 175 zcmaEUntApW<_!%gJl?65X+?>-sV<2niIW?58BCs#Br;jR$xk^mH?t(OEY&$bCnq(z zBr`wHrnsaiGcVmvLkXs5(pH1X4Qcv3A^9bVIWDC|i9ltO6(`9~KA>PY* public class CacheDecorator : IKeyframeExtractor { + private readonly IKeyframeRepository _keyframeRepository; private readonly IKeyframeExtractor _keyframeExtractor; private readonly ILogger _logger; private readonly string _keyframeExtractorName; - private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private readonly string _keyframeCachePath; /// /// Initializes a new instance of the class. /// - /// An instance of the interface. + /// An instance of the interface. /// An instance of the interface. /// An instance of the interface. - public CacheDecorator(IApplicationPaths applicationPaths, IKeyframeExtractor keyframeExtractor, ILogger logger) + public CacheDecorator(IKeyframeRepository keyframeRepository, IKeyframeExtractor keyframeExtractor, ILogger logger) { - ArgumentNullException.ThrowIfNull(applicationPaths); + ArgumentNullException.ThrowIfNull(keyframeRepository); ArgumentNullException.ThrowIfNull(keyframeExtractor); + _keyframeRepository = keyframeRepository; _keyframeExtractor = keyframeExtractor; _logger = logger; _keyframeExtractorName = keyframeExtractor.GetType().Name; - // TODO make the dir configurable - _keyframeCachePath = Path.Combine(applicationPaths.DataPath, "keyframes"); } /// public bool IsMetadataBased => _keyframeExtractor.IsMetadataBased; /// - public bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData) + public bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData) { - keyframeData = null; - var cachePath = GetCachePath(_keyframeCachePath, filePath); - if (TryReadFromCache(cachePath, out var cachedResult)) + keyframeData = _keyframeRepository.GetKeyframeData(itemId).FirstOrDefault(); + if (keyframeData is null) { - keyframeData = cachedResult; - return true; + if (!_keyframeExtractor.TryExtractKeyframes(itemId, filePath, out var result)) + { + _logger.LogDebug("Failed to extract keyframes using {ExtractorName}", _keyframeExtractorName); + return false; + } + + _logger.LogDebug("Successfully extracted keyframes using {ExtractorName}", _keyframeExtractorName); + keyframeData = result; + _keyframeRepository.SaveKeyframeDataAsync(itemId, keyframeData, CancellationToken.None).GetAwaiter().GetResult(); } - if (!_keyframeExtractor.TryExtractKeyframes(filePath, out var result)) - { - _logger.LogDebug("Failed to extract keyframes using {ExtractorName}", _keyframeExtractorName); - return false; - } - - _logger.LogDebug("Successfully extracted keyframes using {ExtractorName}", _keyframeExtractorName); - keyframeData = result; - SaveToCache(cachePath, keyframeData); return true; } - - private static void SaveToCache(string cachePath, KeyframeData keyframeData) - { - var json = JsonSerializer.Serialize(keyframeData, _jsonOptions); - Directory.CreateDirectory(Path.GetDirectoryName(cachePath) ?? throw new ArgumentException($"Provided path ({cachePath}) is not valid.", nameof(cachePath))); - File.WriteAllText(cachePath, json); - } - - private static string GetCachePath(string keyframeCachePath, string filePath) - { - var lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath); - ReadOnlySpan filename = (filePath + "_" + lastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5() + ".json"; - var prefix = filename[..1]; - - return Path.Join(keyframeCachePath, prefix, filename); - } - - private static bool TryReadFromCache(string cachePath, [NotNullWhen(true)] out KeyframeData? cachedResult) - { - if (File.Exists(cachePath)) - { - var bytes = File.ReadAllBytes(cachePath); - cachedResult = JsonSerializer.Deserialize(bytes, _jsonOptions); - return cachedResult is not null; - } - - cachedResult = null; - return false; - } } diff --git a/src/Jellyfin.MediaEncoding.Hls/Extractors/FfProbeKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Hls/Extractors/FfProbeKeyframeExtractor.cs index a8daeeb787..a69746fe0d 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Extractors/FfProbeKeyframeExtractor.cs +++ b/src/Jellyfin.MediaEncoding.Hls/Extractors/FfProbeKeyframeExtractor.cs @@ -34,7 +34,7 @@ public class FfProbeKeyframeExtractor : IKeyframeExtractor public bool IsMetadataBased => false; /// - public bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData) + public bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData) { if (!_namingOptions.VideoFileExtensions.Contains(Path.GetExtension(filePath.AsSpan()), StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Jellyfin.MediaEncoding.Hls/Extractors/IKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Hls/Extractors/IKeyframeExtractor.cs index 083e93de1a..84bccbc721 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Extractors/IKeyframeExtractor.cs +++ b/src/Jellyfin.MediaEncoding.Hls/Extractors/IKeyframeExtractor.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics.CodeAnalysis; using Jellyfin.MediaEncoding.Keyframes; @@ -16,8 +17,9 @@ public interface IKeyframeExtractor /// /// Attempt to extract keyframes. /// + /// The item id. /// The path to the file. /// The keyframes. /// A value indicating whether the keyframe extraction was successful. - bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData); + bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData); } diff --git a/src/Jellyfin.MediaEncoding.Hls/Extractors/MatroskaKeyframeExtractor.cs b/src/Jellyfin.MediaEncoding.Hls/Extractors/MatroskaKeyframeExtractor.cs index 1100f8cd5d..c7758e9193 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Extractors/MatroskaKeyframeExtractor.cs +++ b/src/Jellyfin.MediaEncoding.Hls/Extractors/MatroskaKeyframeExtractor.cs @@ -24,7 +24,7 @@ public class MatroskaKeyframeExtractor : IKeyframeExtractor public bool IsMetadataBased => true; /// - public bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData) + public bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData) { if (!filePath.AsSpan().EndsWith(".mkv", StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj index dc581724a5..80b5aa84e4 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj +++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs b/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs index 21d9bb6588..ac9d30b33a 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs +++ b/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs @@ -1,3 +1,5 @@ +using System; + namespace Jellyfin.MediaEncoding.Hls.Playlist; /// @@ -8,6 +10,7 @@ public class CreateMainPlaylistRequest /// /// Initializes a new instance of the class. /// + /// The media source id. /// The absolute file path to the file. /// The desired segment length in milliseconds. /// The total duration of the file in ticks. @@ -15,8 +18,9 @@ public class CreateMainPlaylistRequest /// The URI prefix for the relative URL in the playlist. /// The desired query string to append (must start with ?). /// Whether the video is being remuxed. - public CreateMainPlaylistRequest(string filePath, int desiredSegmentLengthMs, long totalRuntimeTicks, string segmentContainer, string endpointPrefix, string queryString, bool isRemuxingVideo) + public CreateMainPlaylistRequest(Guid mediaSourceId, string filePath, int desiredSegmentLengthMs, long totalRuntimeTicks, string segmentContainer, string endpointPrefix, string queryString, bool isRemuxingVideo) { + MediaSourceId = mediaSourceId; FilePath = filePath; DesiredSegmentLengthMs = desiredSegmentLengthMs; TotalRuntimeTicks = totalRuntimeTicks; @@ -26,6 +30,11 @@ public class CreateMainPlaylistRequest IsRemuxingVideo = isRemuxingVideo; } + /// + /// Gets the media source id. + /// + public Guid MediaSourceId { get; } + /// /// Gets the file path. /// diff --git a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs index 1846ba26bf..343f3e562d 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs +++ b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs @@ -35,7 +35,7 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator { IReadOnlyList segments; // For video transcodes it is sufficient with equal length segments as ffmpeg will create new keyframes - if (request.IsRemuxingVideo && TryExtractKeyframes(request.FilePath, out var keyframeData)) + if (request.IsRemuxingVideo && TryExtractKeyframes(request.MediaSourceId, request.FilePath, out var keyframeData)) { segments = ComputeSegments(keyframeData, request.DesiredSegmentLengthMs); } @@ -104,7 +104,7 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator return builder.ToString(); } - private bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData) + private bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData) { keyframeData = null; if (!IsExtractionAllowedForFile(filePath, _serverConfigurationManager.GetEncodingOptions().AllowOnDemandMetadataBasedKeyframeExtractionForExtensions)) @@ -116,7 +116,7 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator for (var i = 0; i < len; i++) { var extractor = _extractors[i]; - if (!extractor.TryExtractKeyframes(filePath, out var result)) + if (!extractor.TryExtractKeyframes(itemId, filePath, out var result)) { continue; } diff --git a/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs b/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs index caf6a2aae3..d63ee67774 100644 --- a/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs +++ b/src/Jellyfin.MediaEncoding.Hls/ScheduledTasks/KeyframeExtractionScheduledTask.cs @@ -9,7 +9,6 @@ using Jellyfin.MediaEncoding.Hls.Extractors; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; @@ -23,7 +22,7 @@ public class KeyframeExtractionScheduledTask : IScheduledTask private readonly ILocalizationManager _localizationManager; private readonly ILibraryManager _libraryManager; private readonly IKeyframeExtractor[] _keyframeExtractors; - private static readonly BaseItemKind[] _itemTypes = { BaseItemKind.Episode, BaseItemKind.Movie }; + private static readonly BaseItemKind[] _itemTypes = [BaseItemKind.Episode, BaseItemKind.Movie]; /// /// Initializes a new instance of the class. @@ -55,11 +54,11 @@ public class KeyframeExtractionScheduledTask : IScheduledTask { var query = new InternalItemsQuery { - MediaTypes = new[] { MediaType.Video }, + MediaTypes = [MediaType.Video], IsVirtualItem = false, IncludeItemTypes = _itemTypes, DtoOptions = new DtoOptions(true), - SourceTypes = new[] { SourceType.Library }, + SourceTypes = [SourceType.Library], Recursive = true, Limit = Pagesize }; @@ -74,19 +73,16 @@ public class KeyframeExtractionScheduledTask : IScheduledTask query.StartIndex = startIndex; var videos = _libraryManager.GetItemList(query); - var currentPageCount = videos.Count; - // TODO parallelize with Parallel.ForEach? - for (var i = 0; i < currentPageCount; i++) + foreach (var video in videos) { - var video = videos[i]; // Only local files supported - if (video.IsFileProtocol && File.Exists(video.Path)) + var path = video.Path; + if (File.Exists(path)) { - for (var j = 0; j < _keyframeExtractors.Length; j++) + foreach (var extractor in _keyframeExtractors) { - var extractor = _keyframeExtractors[j]; - // The cache decorator will make sure to save them in the data dir - if (extractor.TryExtractKeyframes(video.Path, out _)) + // The cache decorator will make sure to save the keyframes + if (extractor.TryExtractKeyframes(video.Id, path, out _)) { break; } @@ -107,5 +103,5 @@ public class KeyframeExtractionScheduledTask : IScheduledTask } /// - public IEnumerable GetDefaultTriggers() => Enumerable.Empty(); + public IEnumerable GetDefaultTriggers() => []; } From a18d60d2dedb87735e23e050a2d6d79c395947c1 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 3 Apr 2025 02:19:31 +0200 Subject: [PATCH 166/508] Trim library names (#13828) --- Emby.Server.Implementations/Library/LibraryManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index c8026960df..ab8884f17b 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2899,7 +2899,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(name)); } - name = _fileSystem.GetValidFilename(name); + name = _fileSystem.GetValidFilename(name.Trim()); var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; From 0bde7bae05de1933a9714870c8e4e6f1a946cd93 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Thu, 3 Apr 2025 16:47:31 +0200 Subject: [PATCH 167/508] Only remove keyframe cache dir if it exists (#13834) --- Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs index 1cab943c1a..b8e69db8e7 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs @@ -110,7 +110,10 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine _logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed); - Directory.Delete(KeyframeCachePath, true); + if (Directory.Exists(KeyframeCachePath)) + { + Directory.Delete(KeyframeCachePath, true); + } } private bool TryGetKeyframeData(BaseItem item, [NotNullWhen(true)] out KeyframeData? data) From 596b63551196f7ce9bcb8d8de617d3c79201a375 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Thu, 3 Apr 2025 17:17:14 +0200 Subject: [PATCH 168/508] Cleanup extracted files (#13760) * Cleanup extracted files * Pagination and fixes * Add migration for attachments to MigrateLibraryDb * Unify attachment handling * Don't extract again if files were already extracted * Fix MKS attachment extraction * Always run full extraction on mks * Don't try to extract mjpeg streams as attachments * Fallback to check if attachments were extracted to cache folder * Fixup --- .../Data/CleanDatabaseScheduledTask.cs | 41 +- .../Library/LibraryManager.cs | 17 + .../Library/PathManager.cs | 40 +- Jellyfin.Server/Migrations/MigrationRunner.cs | 1 + .../Migrations/Routines/MigrateLibraryDb.cs | 66 + .../Migrations/Routines/MoveExtractedFiles.cs | 299 +++ MediaBrowser.Controller/IO/IPathManager.cs | 32 + .../MediaEncoding/EncodingHelper.cs | 8 +- .../MediaEncoding/IAttachmentExtractor.cs | 47 +- .../Attachments/AttachmentExtractor.cs | 442 ++--- .../Subtitles/SubtitleEncoder.cs | 33 +- .../Transcoding/TranscodeManager.cs | 11 +- .../Entities/MediaAttachment.cs | 80 +- .../Entities/AttachmentStreamInfo.cs | 2 +- ...1182844_FixAttachmentMigration.Designer.cs | 1657 +++++++++++++++++ .../20250331182844_FixAttachmentMigration.cs | 36 + .../Migrations/JellyfinDbModelSnapshot.cs | Bin 59931 -> 59893 bytes 17 files changed, 2397 insertions(+), 415 deletions(-) create mode 100644 Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.Designer.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 63481b1f8c..9a80eafe50 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -1,14 +1,14 @@ #pragma warning disable CS1591 using System; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations; -using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Trickplay; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -19,15 +19,18 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; private readonly IDbContextFactory _dbProvider; + private readonly IPathManager _pathManager; public CleanDatabaseScheduledTask( ILibraryManager libraryManager, ILogger logger, - IDbContextFactory dbProvider) + IDbContextFactory dbProvider, + IPathManager pathManager) { _libraryManager = libraryManager; _logger = logger; _dbProvider = dbProvider; + _pathManager = pathManager; } public async Task Run(IProgress progress, CancellationToken cancellationToken) @@ -56,6 +59,38 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask { _logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty); + foreach (var mediaSource in item.GetMediaSources(false)) + { + // Delete extracted subtitles + try + { + var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id); + if (Directory.Exists(subtitleFolder)) + { + Directory.Delete(subtitleFolder, true); + } + } + catch (Exception e) + { + _logger.LogWarning("Failed to remove subtitle cache folder for {Item}: {Exception}", item.Id, e.Message); + } + + // Delete extracted attachments + try + { + var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id); + if (Directory.Exists(attachmentFolder)) + { + Directory.Delete(attachmentFolder, true); + } + } + catch (Exception e) + { + _logger.LogWarning("Failed to remove attachment cache folder for {Item}: {Exception}", item.Id, e.Message); + } + } + + // Delete item _libraryManager.DeleteItem(item, new DeleteOptions { DeleteFileLocation = false diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index ab8884f17b..1303bb3cb2 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -492,7 +492,24 @@ namespace Emby.Server.Implementations.Library if (item is Video video) { + // Trickplay list.Add(_pathManager.GetTrickplayDirectory(video)); + + // Subtitles and attachments + foreach (var mediaSource in item.GetMediaSources(false)) + { + var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id); + if (subtitleFolder is not null) + { + list.Add(subtitleFolder); + } + + var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id); + if (attachmentFolder is not null) + { + list.Add(attachmentFolder); + } + } } return list; diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs index c910abadbc..ac004b4132 100644 --- a/Emby.Server.Implementations/Library/PathManager.cs +++ b/Emby.Server.Implementations/Library/PathManager.cs @@ -1,5 +1,7 @@ +using System; using System.Globalization; using System.IO; +using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; @@ -12,22 +14,56 @@ namespace Emby.Server.Implementations.Library; public class PathManager : IPathManager { private readonly IServerConfigurationManager _config; + private readonly IApplicationPaths _appPaths; /// /// Initializes a new instance of the class. /// /// The server configuration manager. + /// The application paths. public PathManager( - IServerConfigurationManager config) + IServerConfigurationManager config, + IApplicationPaths appPaths) { _config = config; + _appPaths = appPaths; + } + + private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); + + private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments"); + + /// + public string GetAttachmentPath(string mediaSourceId, string fileName) + { + return Path.Join(GetAttachmentFolderPath(mediaSourceId), fileName); + } + + /// + public string GetAttachmentFolderPath(string mediaSourceId) + { + var id = Guid.Parse(mediaSourceId); + return Path.Join(AttachmentCachePath, id.ToString("D", CultureInfo.InvariantCulture)); + } + + /// + public string GetSubtitleFolderPath(string mediaSourceId) + { + var id = Guid.Parse(mediaSourceId); + return Path.Join(SubtitleCachePath, id.ToString("D", CultureInfo.InvariantCulture)); + } + + /// + public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension) + { + return Path.Join(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension); } /// public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false) { var basePath = _config.ApplicationPaths.TrickplayPath; - var idString = item.Id.ToString("N", CultureInfo.InvariantCulture); + var idString = item.Id.ToString("D", CultureInfo.InvariantCulture); return saveWithMedia ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index baeea2c14a..c3a2e1bc4e 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -54,6 +54,7 @@ namespace Jellyfin.Server.Migrations typeof(Routines.FixAudioData), typeof(Routines.RemoveDuplicatePlaylistChildren), typeof(Routines.MigrateLibraryDb), + typeof(Routines.MoveExtractedFiles), typeof(Routines.MigrateRatingLevels), typeof(Routines.MoveTrickplayFiles), typeof(Routines.MigrateKeyframeData), diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index f414b6e396..3fc9bea842 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -80,6 +80,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine using (var operation = GetPreparedDbContext("Cleanup database")) { + operation.JellyfinDbContext.AttachmentStreamInfos.ExecuteDelete(); operation.JellyfinDbContext.BaseItems.ExecuteDelete(); operation.JellyfinDbContext.ItemValues.ExecuteDelete(); operation.JellyfinDbContext.UserData.ExecuteDelete(); @@ -251,6 +252,29 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine } } + using (var operation = GetPreparedDbContext("moving AttachmentStreamInfos")) + { + const string mediaAttachmentQuery = + """ + SELECT ItemId, AttachmentIndex, Codec, CodecTag, Comment, filename, MIMEType + FROM mediaattachments + WHERE EXISTS(SELECT 1 FROM TypedBaseItems WHERE TypedBaseItems.guid = mediaattachments.ItemId) + """; + + using (new TrackedMigrationStep("loading AttachmentStreamInfos", _logger)) + { + foreach (SqliteDataReader dto in connection.Query(mediaAttachmentQuery)) + { + operation.JellyfinDbContext.AttachmentStreamInfos.Add(GetMediaAttachment(dto)); + } + } + + using (new TrackedMigrationStep($"saving {operation.JellyfinDbContext.AttachmentStreamInfos.Local.Count} AttachmentStreamInfos entries", _logger)) + { + operation.JellyfinDbContext.SaveChanges(); + } + } + using (var operation = GetPreparedDbContext("moving People")) { const string personsQuery = @@ -709,6 +733,48 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine return item; } + /// + /// Gets the attachment. + /// + /// The reader. + /// MediaAttachment. + private AttachmentStreamInfo GetMediaAttachment(SqliteDataReader reader) + { + var item = new AttachmentStreamInfo + { + Index = reader.GetInt32(1), + Item = null!, + ItemId = reader.GetGuid(0), + }; + + if (reader.TryGetString(2, out var codec)) + { + item.Codec = codec; + } + + if (reader.TryGetString(3, out var codecTag)) + { + item.CodecTag = codecTag; + } + + if (reader.TryGetString(4, out var comment)) + { + item.Comment = comment; + } + + if (reader.TryGetString(5, out var fileName)) + { + item.Filename = fileName; + } + + if (reader.TryGetString(6, out var mimeType)) + { + item.MimeType = mimeType; + } + + return item; + } + private (BaseItemEntity BaseItem, string[] LegacyUserDataKey) GetItem(SqliteDataReader reader) { var entity = new BaseItemEntity() diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs new file mode 100644 index 0000000000..f63c5fd409 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs @@ -0,0 +1,299 @@ +#pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations.Routines; + +/// +/// Migration to move extracted files to the new directories. +/// +public class MoveExtractedFiles : IDatabaseMigrationRoutine +{ + private readonly IApplicationPaths _appPaths; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IPathManager _pathManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// The logger. + /// Instance of the interface. + /// Instance of the interface. + public MoveExtractedFiles( + IApplicationPaths appPaths, + ILibraryManager libraryManager, + ILogger logger, + IMediaSourceManager mediaSourceManager, + IPathManager pathManager) + { + _appPaths = appPaths; + _libraryManager = libraryManager; + _logger = logger; + _mediaSourceManager = mediaSourceManager; + _pathManager = pathManager; + } + + private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); + + private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments"); + + /// + public Guid Id => new("9063b0Ef-CFF1-4EDC-9A13-74093681A89B"); + + /// + public string Name => "MoveExtractedFiles"; + + /// + public bool PerformOnNewInstall => false; + + /// + public void Perform() + { + const int Limit = 500; + int itemCount = 0, offset = 0; + + var sw = Stopwatch.StartNew(); + var itemsQuery = new InternalItemsQuery + { + MediaTypes = [MediaType.Video], + SourceTypes = [SourceType.Library], + IsVirtualItem = false, + IsFolder = false, + Limit = Limit, + StartIndex = offset, + EnableTotalRecordCount = true, + }; + + var records = _libraryManager.GetItemsResult(itemsQuery).TotalRecordCount; + _logger.LogInformation("Checking {Count} items for movable extracted files.", records); + + // Make sure directories exist + Directory.CreateDirectory(SubtitleCachePath); + Directory.CreateDirectory(AttachmentCachePath); + + itemsQuery.EnableTotalRecordCount = false; + do + { + itemsQuery.StartIndex = offset; + var result = _libraryManager.GetItemsResult(itemsQuery); + + var items = result.Items; + foreach (var item in items) + { + if (MoveSubtitleAndAttachmentFiles(item)) + { + itemCount++; + } + } + + offset += Limit; + if (offset % 5_000 == 0) + { + _logger.LogInformation("Checked extracted files for {Count} items in {Time}.", offset, sw.Elapsed); + } + } while (offset < records); + + _logger.LogInformation("Checked {Checked} items - Moved files for {Items} items in {Time}.", records, itemCount, sw.Elapsed); + + // Get all subdirectories with 1 character names (those are the legacy directories) + var subdirectories = Directory.GetDirectories(SubtitleCachePath, "*", SearchOption.AllDirectories).Where(s => s.Length == SubtitleCachePath.Length + 2).ToList(); + subdirectories.AddRange(Directory.GetDirectories(AttachmentCachePath, "*", SearchOption.AllDirectories).Where(s => s.Length == AttachmentCachePath.Length + 2)); + + // Remove all legacy subdirectories + foreach (var subdir in subdirectories) + { + Directory.Delete(subdir, true); + } + + // Remove old cache path + var attachmentCachePath = Path.Join(_appPaths.CachePath, "attachments"); + if (Directory.Exists(attachmentCachePath)) + { + Directory.Delete(attachmentCachePath, true); + } + + _logger.LogInformation("Cleaned up left over subtitles and attachments."); + } + + private bool MoveSubtitleAndAttachmentFiles(BaseItem item) + { + var mediaStreams = item.GetMediaStreams().Where(s => s.Type == MediaStreamType.Subtitle && !s.IsExternal); + var itemIdString = item.Id.ToString("N", CultureInfo.InvariantCulture); + var modified = false; + foreach (var mediaStream in mediaStreams) + { + if (mediaStream.Codec is null) + { + continue; + } + + var mediaStreamIndex = mediaStream.Index; + var extension = GetSubtitleExtension(mediaStream.Codec); + var oldSubtitleCachePath = GetOldSubtitleCachePath(item.Path, mediaStream.Index, extension); + if (string.IsNullOrEmpty(oldSubtitleCachePath) || !File.Exists(oldSubtitleCachePath)) + { + continue; + } + + var newSubtitleCachePath = _pathManager.GetSubtitlePath(itemIdString, mediaStreamIndex, extension); + if (File.Exists(newSubtitleCachePath)) + { + File.Delete(oldSubtitleCachePath); + } + else + { + var newDirectory = Path.GetDirectoryName(newSubtitleCachePath); + if (newDirectory is not null) + { + Directory.CreateDirectory(newDirectory); + File.Move(oldSubtitleCachePath, newSubtitleCachePath, false); + _logger.LogDebug("Moved subtitle {Index} for {Item} from {Source} to {Destination}", mediaStreamIndex, item.Id, oldSubtitleCachePath, newSubtitleCachePath); + + modified = true; + } + } + } + + var attachments = _mediaSourceManager.GetMediaAttachments(item.Id).Where(a => !string.Equals(a.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)).ToList(); + var shouldExtractOneByOne = attachments.Any(a => !string.IsNullOrEmpty(a.FileName) + && (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase))); + foreach (var attachment in attachments) + { + var attachmentIndex = attachment.Index; + var oldAttachmentPath = GetOldAttachmentDataPath(item.Path, attachmentIndex); + if (string.IsNullOrEmpty(oldAttachmentPath) || !File.Exists(oldAttachmentPath)) + { + oldAttachmentPath = GetOldAttachmentCachePath(itemIdString, attachment, shouldExtractOneByOne); + if (string.IsNullOrEmpty(oldAttachmentPath) || !File.Exists(oldAttachmentPath)) + { + continue; + } + } + + var newAttachmentPath = _pathManager.GetAttachmentPath(itemIdString, attachment.FileName ?? attachmentIndex.ToString(CultureInfo.InvariantCulture)); + if (File.Exists(newAttachmentPath)) + { + File.Delete(oldAttachmentPath); + } + else + { + var newDirectory = Path.GetDirectoryName(newAttachmentPath); + if (newDirectory is not null) + { + Directory.CreateDirectory(newDirectory); + File.Move(oldAttachmentPath, newAttachmentPath, false); + _logger.LogDebug("Moved attachment {Index} for {Item} from {Source} to {Destination}", attachmentIndex, item.Id, oldAttachmentPath, newAttachmentPath); + + modified = true; + } + } + } + + return modified; + } + + private string? GetOldAttachmentDataPath(string? mediaPath, int attachmentStreamIndex) + { + if (mediaPath is null) + { + return null; + } + + string filename; + var protocol = _mediaSourceManager.GetPathProtocol(mediaPath); + if (protocol == MediaProtocol.File) + { + DateTime? date; + try + { + date = File.GetLastWriteTimeUtc(mediaPath); + } + catch (IOException e) + { + _logger.LogDebug("Skipping attachment at index {Index} for {Path}: {Exception}", attachmentStreamIndex, mediaPath, e.Message); + + return null; + } + + filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); + } + else + { + filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); + } + + return Path.Join(_appPaths.DataPath, "attachments", filename[..1], filename); + } + + private string? GetOldAttachmentCachePath(string mediaSourceId, MediaAttachment attachment, bool shouldExtractOneByOne) + { + var attachmentFolderPath = Path.Join(_appPaths.CachePath, "attachments", mediaSourceId); + if (shouldExtractOneByOne) + { + return Path.Join(attachmentFolderPath, attachment.Index.ToString(CultureInfo.InvariantCulture)); + } + + if (string.IsNullOrEmpty(attachment.FileName)) + { + return null; + } + + return Path.Join(attachmentFolderPath, attachment.FileName); + } + + private string? GetOldSubtitleCachePath(string path, int streamIndex, string outputSubtitleExtension) + { + DateTime? date; + try + { + date = File.GetLastWriteTimeUtc(path); + } + catch (IOException e) + { + _logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message); + + return null; + } + + var ticksParam = string.Empty; + ReadOnlySpan filename = new Guid(MD5.HashData(Encoding.Unicode.GetBytes(path + "_" + streamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Value.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam))) + outputSubtitleExtension; + + return Path.Join(SubtitleCachePath, filename[..1], filename); + } + + private static string GetSubtitleExtension(string codec) + { + if (codec.ToLower(CultureInfo.InvariantCulture).Equals("ass", StringComparison.OrdinalIgnoreCase) + || codec.ToLower(CultureInfo.InvariantCulture).Equals("ssa", StringComparison.OrdinalIgnoreCase)) + { + return "." + codec; + } + else if (codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)) + { + return ".sup"; + } + else + { + return ".srt"; + } + } +} diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs index 0368898102..7c20164a6f 100644 --- a/MediaBrowser.Controller/IO/IPathManager.cs +++ b/MediaBrowser.Controller/IO/IPathManager.cs @@ -1,4 +1,5 @@ using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.IO; @@ -14,4 +15,35 @@ public interface IPathManager /// Whether or not the tile should be saved next to the media file. /// The absolute path. public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false); + + /// + /// Gets the path to the subtitle file. + /// + /// The media source id. + /// The stream index. + /// The subtitle file extension. + /// The absolute path. + public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension); + + /// + /// Gets the path to the subtitle file. + /// + /// The media source id. + /// The absolute path. + public string GetSubtitleFolderPath(string mediaSourceId); + + /// + /// Gets the path to the attachment file. + /// + /// The media source id. + /// The attachmentFileName index. + /// The absolute path. + public string GetAttachmentPath(string mediaSourceId, string fileName); + + /// + /// Gets the path to the attachment folder. + /// + /// The media source id. + /// The absolute path. + public string GetAttachmentFolderPath(string mediaSourceId); } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index afa962a41c..75b3f151d7 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -19,6 +19,7 @@ using Jellyfin.Database.Implementations.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; +using MediaBrowser.Controller.IO; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; @@ -55,6 +56,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly ISubtitleEncoder _subtitleEncoder; private readonly IConfiguration _config; private readonly IConfigurationManager _configurationManager; + private readonly IPathManager _pathManager; // i915 hang was fixed by linux 6.2 (3f882f2) private readonly Version _minKerneli915Hang = new Version(5, 18); @@ -153,13 +155,15 @@ namespace MediaBrowser.Controller.MediaEncoding IMediaEncoder mediaEncoder, ISubtitleEncoder subtitleEncoder, IConfiguration config, - IConfigurationManager configurationManager) + IConfigurationManager configurationManager, + IPathManager pathManager) { _appPaths = appPaths; _mediaEncoder = mediaEncoder; _subtitleEncoder = subtitleEncoder; _config = config; _configurationManager = configurationManager; + _pathManager = pathManager; } private enum DynamicHdrMetadataRemovalPlan @@ -1785,7 +1789,7 @@ namespace MediaBrowser.Controller.MediaEncoding var alphaParam = enableAlpha ? ":alpha=1" : string.Empty; var sub2videoParam = enableSub2video ? ":sub2video=1" : string.Empty; - var fontPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id); + var fontPath = _pathManager.GetAttachmentFolderPath(state.MediaSource.Id); var fontParam = string.Format( CultureInfo.InvariantCulture, ":fontsdir='{0}'", diff --git a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs index 09840d2eea..d8d1364727 100644 --- a/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs +++ b/MediaBrowser.Controller/MediaEncoding/IAttachmentExtractor.cs @@ -9,26 +9,33 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Controller.MediaEncoding +namespace MediaBrowser.Controller.MediaEncoding; + +public interface IAttachmentExtractor { - public interface IAttachmentExtractor - { - Task<(MediaAttachment Attachment, Stream Stream)> GetAttachment( - BaseItem item, - string mediaSourceId, - int attachmentStreamIndex, - CancellationToken cancellationToken); + /// + /// Gets the path to the attachment file. + /// + /// The . + /// The media source id. + /// The attachment index. + /// The cancellation token. + /// The async task. + Task<(MediaAttachment Attachment, Stream Stream)> GetAttachment( + BaseItem item, + string mediaSourceId, + int attachmentStreamIndex, + CancellationToken cancellationToken); - Task ExtractAllAttachments( - string inputFile, - MediaSourceInfo mediaSource, - string outputPath, - CancellationToken cancellationToken); - - Task ExtractAllAttachmentsExternal( - string inputArgument, - string id, - string outputPath, - CancellationToken cancellationToken); - } + /// + /// Gets the path to the attachment file. + /// + /// The input file path. + /// The source id. + /// The cancellation token. + /// The async task. + Task ExtractAllAttachments( + string inputFile, + MediaSourceInfo mediaSource, + CancellationToken cancellationToken); } diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 431fc0b178..89291c73bf 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -1,7 +1,4 @@ -#pragma warning disable CS1591 - using System; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; @@ -9,28 +6,27 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; -using MediaBrowser.Common; -using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Encoder; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Attachments { + /// public sealed class AttachmentExtractor : IAttachmentExtractor, IDisposable { private readonly ILogger _logger; - private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly IMediaEncoder _mediaEncoder; private readonly IMediaSourceManager _mediaSourceManager; + private readonly IPathManager _pathManager; private readonly AsyncKeyedLocker _semaphoreLocks = new(o => { @@ -38,18 +34,26 @@ namespace MediaBrowser.MediaEncoding.Attachments o.PoolInitialFill = 1; }); + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The . + /// The . + /// The . + /// The . public AttachmentExtractor( ILogger logger, - IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, - IMediaSourceManager mediaSourceManager) + IMediaSourceManager mediaSourceManager, + IPathManager pathManager) { _logger = logger; - _appPaths = appPaths; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; _mediaSourceManager = mediaSourceManager; + _pathManager = pathManager; } /// @@ -77,157 +81,151 @@ namespace MediaBrowser.MediaEncoding.Attachments throw new ResourceNotFoundException($"MediaSource {mediaSourceId} has no attachment with stream index {attachmentStreamIndex}"); } + if (string.Equals(mediaAttachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)) + { + throw new ResourceNotFoundException($"Attachment with stream index {attachmentStreamIndex} can't be extracted for MediaSource {mediaSourceId}"); + } + var attachmentStream = await GetAttachmentStream(mediaSource, mediaAttachment, cancellationToken) .ConfigureAwait(false); return (mediaAttachment, attachmentStream); } + /// public async Task ExtractAllAttachments( string inputFile, MediaSourceInfo mediaSource, - string outputPath, CancellationToken cancellationToken) { var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => !string.IsNullOrEmpty(a.FileName) && (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase))); - if (shouldExtractOneByOne) + if (shouldExtractOneByOne && !inputFile.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) { - var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index); - foreach (var i in attachmentIndexes) + foreach (var attachment in mediaSource.MediaAttachments) { - var newName = Path.Join(outputPath, i.ToString(CultureInfo.InvariantCulture)); - await ExtractAttachment(inputFile, mediaSource, i, newName, cancellationToken).ConfigureAwait(false); + if (!string.Equals(attachment.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)) + { + await ExtractAttachment(inputFile, mediaSource, attachment, cancellationToken).ConfigureAwait(false); + } } } else { - using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false)) - { - if (!Directory.Exists(outputPath)) - { - await ExtractAllAttachmentsInternal( - _mediaEncoder.GetInputArgument(inputFile, mediaSource), - outputPath, - false, - cancellationToken).ConfigureAwait(false); - } - } - } - } - - public async Task ExtractAllAttachmentsExternal( - string inputArgument, - string id, - string outputPath, - CancellationToken cancellationToken) - { - using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false)) - { - if (!File.Exists(Path.Join(outputPath, id))) - { - await ExtractAllAttachmentsInternal( - inputArgument, - outputPath, - true, - cancellationToken).ConfigureAwait(false); - - if (Directory.Exists(outputPath)) - { - File.Create(Path.Join(outputPath, id)); - } - } + await ExtractAllAttachmentsInternal( + inputFile, + mediaSource, + false, + cancellationToken).ConfigureAwait(false); } } private async Task ExtractAllAttachmentsInternal( - string inputPath, - string outputPath, + string inputFile, + MediaSourceInfo mediaSource, bool isExternal, CancellationToken cancellationToken) { + var inputPath = _mediaEncoder.GetInputArgument(inputFile, mediaSource); + ArgumentException.ThrowIfNullOrEmpty(inputPath); - ArgumentException.ThrowIfNullOrEmpty(outputPath); - Directory.CreateDirectory(outputPath); - - var processArgs = string.Format( - CultureInfo.InvariantCulture, - "-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null", - inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty, - inputPath); - - int exitCode; - - using (var process = new Process - { - StartInfo = new ProcessStartInfo - { - Arguments = processArgs, - FileName = _mediaEncoder.EncoderPath, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - WorkingDirectory = outputPath, - ErrorDialog = false - }, - EnableRaisingEvents = true - }) + var outputFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id); + using (await _semaphoreLocks.LockAsync(outputFolder, cancellationToken).ConfigureAwait(false)) { - _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); - - process.Start(); - - try + if (!Directory.Exists(outputFolder)) { - await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); - exitCode = process.ExitCode; - } - catch (OperationCanceledException) - { - process.Kill(true); - exitCode = -1; - } - } - - var failed = false; - - if (exitCode != 0) - { - if (isExternal && exitCode == 1) - { - // ffmpeg returns exitCode 1 because there is no video or audio stream - // this can be ignored + Directory.CreateDirectory(outputFolder); } else { - failed = true; - - _logger.LogWarning("Deleting extracted attachments {Path} due to failure: {ExitCode}", outputPath, exitCode); - try + var fileNames = Directory.GetFiles(outputFolder, "*", SearchOption.TopDirectoryOnly).Select(f => Path.GetFileName(f)); + var missingFiles = mediaSource.MediaAttachments.Where(a => !fileNames.Contains(a.FileName) && !string.Equals(a.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)); + if (!missingFiles.Any()) { - Directory.Delete(outputPath); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting extracted attachments {Path}", outputPath); + // Skip extraction if all files already exist + return; } } - } - else if (!Directory.Exists(outputPath)) - { - failed = true; - } - if (failed) - { - _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath); + var processArgs = string.Format( + CultureInfo.InvariantCulture, + "-dump_attachment:t \"\" -y {0} -i {1} -t 0 -f null null", + inputPath.EndsWith(".concat\"", StringComparison.OrdinalIgnoreCase) ? "-f concat -safe 0" : string.Empty, + inputPath); - throw new InvalidOperationException( - string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath)); + int exitCode; + + using (var process = new Process + { + StartInfo = new ProcessStartInfo + { + Arguments = processArgs, + FileName = _mediaEncoder.EncoderPath, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + WorkingDirectory = outputFolder, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) + { + _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); + + process.Start(); + + try + { + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + exitCode = process.ExitCode; + } + catch (OperationCanceledException) + { + process.Kill(true); + exitCode = -1; + } + } + + var failed = false; + + if (exitCode != 0) + { + if (isExternal && exitCode == 1) + { + // ffmpeg returns exitCode 1 because there is no video or audio stream + // this can be ignored + } + else + { + failed = true; + + _logger.LogWarning("Deleting extracted attachments {Path} due to failure: {ExitCode}", outputFolder, exitCode); + try + { + Directory.Delete(outputFolder); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting extracted attachments {Path}", outputFolder); + } + } + } + else if (!Directory.Exists(outputFolder)) + { + failed = true; + } + + if (failed) + { + _logger.LogError("ffmpeg attachment extraction failed for {InputPath} to {OutputPath}", inputPath, outputFolder); + + throw new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputFolder)); + } + + _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputFolder); } - - _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); } private async Task GetAttachmentStream( @@ -235,192 +233,31 @@ namespace MediaBrowser.MediaEncoding.Attachments MediaAttachment mediaAttachment, CancellationToken cancellationToken) { - var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource, mediaAttachment, cancellationToken).ConfigureAwait(false); + var attachmentPath = await ExtractAttachment(mediaSource.Path, mediaSource, mediaAttachment, cancellationToken) + .ConfigureAwait(false); return AsyncFile.OpenRead(attachmentPath); } - private async Task GetReadableFile( - string mediaPath, + private async Task ExtractAttachment( string inputFile, MediaSourceInfo mediaSource, MediaAttachment mediaAttachment, CancellationToken cancellationToken) { - await CacheAllAttachments(mediaPath, inputFile, mediaSource, cancellationToken).ConfigureAwait(false); - - var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, mediaAttachment.Index); - await ExtractAttachment(inputFile, mediaSource, mediaAttachment.Index, outputPath, cancellationToken) - .ConfigureAwait(false); - - return outputPath; - } - - private async Task CacheAllAttachments( - string mediaPath, - string inputFile, - MediaSourceInfo mediaSource, - CancellationToken cancellationToken) - { - var outputFileLocks = new List(); - var extractableAttachmentIds = new List(); - - try + var attachmentFolderPath = _pathManager.GetAttachmentFolderPath(mediaSource.Id); + using (await _semaphoreLocks.LockAsync(attachmentFolderPath, cancellationToken).ConfigureAwait(false)) { - foreach (var attachment in mediaSource.MediaAttachments) - { - var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, attachment.Index); - - var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false); - - if (File.Exists(outputPath)) - { - releaser.Dispose(); - continue; - } - - outputFileLocks.Add(releaser); - extractableAttachmentIds.Add(attachment.Index); - } - - if (extractableAttachmentIds.Count > 0) - { - await CacheAllAttachmentsInternal(mediaPath, _mediaEncoder.GetInputArgument(inputFile, mediaSource), mediaSource, extractableAttachmentIds, cancellationToken).ConfigureAwait(false); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Unable to cache media attachments for File:{File}", mediaPath); - } - finally - { - outputFileLocks.ForEach(x => x.Dispose()); - } - } - - private async Task CacheAllAttachmentsInternal( - string mediaPath, - string inputFile, - MediaSourceInfo mediaSource, - List extractableAttachmentIds, - CancellationToken cancellationToken) - { - var outputPaths = new List(); - var processArgs = string.Empty; - - foreach (var attachmentId in extractableAttachmentIds) - { - var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, attachmentId); - - Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid.")); - - outputPaths.Add(outputPath); - processArgs += string.Format( - CultureInfo.InvariantCulture, - " -dump_attachment:{0} \"{1}\"", - attachmentId, - EncodingUtils.NormalizePath(outputPath)); - } - - processArgs += string.Format( - CultureInfo.InvariantCulture, - " -i {0} -t 0 -f null null", - inputFile); - - int exitCode; - - using (var process = new Process - { - StartInfo = new ProcessStartInfo - { - Arguments = processArgs, - FileName = _mediaEncoder.EncoderPath, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }, - EnableRaisingEvents = true - }) - { - _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); - - process.Start(); - - try - { - await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); - exitCode = process.ExitCode; - } - catch (OperationCanceledException) - { - process.Kill(true); - exitCode = -1; - } - } - - var failed = false; - - if (exitCode == -1) - { - failed = true; - - foreach (var outputPath in outputPaths) - { - try - { - _logger.LogWarning("Deleting extracted media attachment due to failure: {Path}", outputPath); - _fileSystem.DeleteFile(outputPath); - } - catch (FileNotFoundException) - { - // ffmpeg failed, so it is normal that one or more expected output files do not exist. - // There is no need to log anything for the user here. - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting extracted media attachment {Path}", outputPath); - } - } - } - else - { - foreach (var outputPath in outputPaths) - { - if (!File.Exists(outputPath)) - { - _logger.LogError("ffmpeg media attachment extraction failed for {InputPath} to {OutputPath}", inputFile, outputPath); - failed = true; - continue; - } - - _logger.LogInformation("ffmpeg media attachment extraction completed for {InputPath} to {OutputPath}", inputFile, outputPath); - } - } - - if (failed) - { - throw new FfmpegException( - string.Format(CultureInfo.InvariantCulture, "ffmpeg media attachment extraction failed for {0}", inputFile)); - } - } - - private async Task ExtractAttachment( - string inputFile, - MediaSourceInfo mediaSource, - int attachmentStreamIndex, - string outputPath, - CancellationToken cancellationToken) - { - using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false)) - { - if (!File.Exists(outputPath)) + var attachmentPath = _pathManager.GetAttachmentPath(mediaSource.Id, mediaAttachment.FileName ?? mediaAttachment.Index.ToString(CultureInfo.InvariantCulture)); + if (!File.Exists(attachmentPath)) { await ExtractAttachmentInternal( _mediaEncoder.GetInputArgument(inputFile, mediaSource), - attachmentStreamIndex, - outputPath, + mediaAttachment.Index, + attachmentPath, cancellationToken).ConfigureAwait(false); } + + return attachmentPath; } } @@ -510,23 +347,6 @@ namespace MediaBrowser.MediaEncoding.Attachments _logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath); } - private string GetAttachmentCachePath(string mediaPath, MediaSourceInfo mediaSource, int attachmentStreamIndex) - { - string filename; - if (mediaSource.Protocol == MediaProtocol.File) - { - var date = _fileSystem.GetLastWriteTimeUtc(mediaPath); - filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); - } - else - { - filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); - } - - var prefix = filename.AsSpan(0, 1); - return Path.Join(_appPaths.DataPath, "attachments", prefix, filename); - } - /// public void Dispose() { diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index a731d4785b..777e335874 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -13,10 +13,10 @@ using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; using MediaBrowser.Common; -using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dto; @@ -31,12 +31,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles public sealed class SubtitleEncoder : ISubtitleEncoder, IDisposable { private readonly ILogger _logger; - private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly IMediaEncoder _mediaEncoder; private readonly IHttpClientFactory _httpClientFactory; private readonly IMediaSourceManager _mediaSourceManager; private readonly ISubtitleParser _subtitleParser; + private readonly IPathManager _pathManager; /// /// The _semaphoreLocks. @@ -49,24 +49,22 @@ namespace MediaBrowser.MediaEncoding.Subtitles public SubtitleEncoder( ILogger logger, - IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IHttpClientFactory httpClientFactory, IMediaSourceManager mediaSourceManager, - ISubtitleParser subtitleParser) + ISubtitleParser subtitleParser, + IPathManager pathManager) { _logger = logger; - _appPaths = appPaths; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; _httpClientFactory = httpClientFactory; _mediaSourceManager = mediaSourceManager; _subtitleParser = subtitleParser; + _pathManager = pathManager; } - private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); - private MemoryStream ConvertSubtitles( Stream stream, string inputFormat, @@ -830,26 +828,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles private string GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension) { - if (mediaSource.Protocol == MediaProtocol.File) - { - var ticksParam = string.Empty; - - var date = _fileSystem.GetLastWriteTimeUtc(mediaSource.Path); - - ReadOnlySpan filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension; - - var prefix = filename.Slice(0, 1); - - return Path.Join(SubtitleCachePath, prefix, filename); - } - else - { - ReadOnlySpan filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension; - - var prefix = filename.Slice(0, 1); - - return Path.Join(SubtitleCachePath, prefix, filename); - } + return _pathManager.GetSubtitlePath(mediaSource.Id, subtitleStreamIndex, outputSubtitleExtension); } /// diff --git a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs index c7f9cf2ccf..0cda803d64 100644 --- a/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs +++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs @@ -398,24 +398,19 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable // If subtitles get burned in fonts may need to be extracted from the media file if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) { - var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id); if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay) { var concatPath = Path.Join(_appPaths.CachePath, "concat", state.MediaSource.Id + ".concat"); - await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); + await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, cancellationTokenSource.Token).ConfigureAwait(false); } else { - await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); + await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, cancellationTokenSource.Token).ConfigureAwait(false); } if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase)) { - string subtitlePath = state.SubtitleStream.Path; - string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal)); - string subtitleId = subtitlePath.GetMD5().ToString("N", CultureInfo.InvariantCulture); - - await _attachmentExtractor.ExtractAllAttachmentsExternal(subtitlePathArgument, subtitleId, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); + await _attachmentExtractor.ExtractAllAttachments(state.SubtitleStream.Path, state.MediaSource, cancellationTokenSource.Token).ConfigureAwait(false); } } diff --git a/MediaBrowser.Model/Entities/MediaAttachment.cs b/MediaBrowser.Model/Entities/MediaAttachment.cs index 34e3eabc97..f8f7ad0f9f 100644 --- a/MediaBrowser.Model/Entities/MediaAttachment.cs +++ b/MediaBrowser.Model/Entities/MediaAttachment.cs @@ -1,51 +1,49 @@ -#nullable disable -namespace MediaBrowser.Model.Entities +namespace MediaBrowser.Model.Entities; + +/// +/// Class MediaAttachment. +/// +public class MediaAttachment { /// - /// Class MediaAttachment. + /// Gets or sets the codec. /// - public class MediaAttachment - { - /// - /// Gets or sets the codec. - /// - /// The codec. - public string Codec { get; set; } + /// The codec. + public string? Codec { get; set; } - /// - /// Gets or sets the codec tag. - /// - /// The codec tag. - public string CodecTag { get; set; } + /// + /// Gets or sets the codec tag. + /// + /// The codec tag. + public string? CodecTag { get; set; } - /// - /// Gets or sets the comment. - /// - /// The comment. - public string Comment { get; set; } + /// + /// Gets or sets the comment. + /// + /// The comment. + public string? Comment { get; set; } - /// - /// Gets or sets the index. - /// - /// The index. - public int Index { get; set; } + /// + /// Gets or sets the index. + /// + /// The index. + public int Index { get; set; } - /// - /// Gets or sets the filename. - /// - /// The filename. - public string FileName { get; set; } + /// + /// Gets or sets the filename. + /// + /// The filename. + public string? FileName { get; set; } - /// - /// Gets or sets the MIME type. - /// - /// The MIME type. - public string MimeType { get; set; } + /// + /// Gets or sets the MIME type. + /// + /// The MIME type. + public string? MimeType { get; set; } - /// - /// Gets or sets the delivery URL. - /// - /// The delivery URL. - public string DeliveryUrl { get; set; } - } + /// + /// Gets or sets the delivery URL. + /// + /// The delivery URL. + public string? DeliveryUrl { get; set; } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs index aab3082b37..2f27d9389a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs @@ -25,7 +25,7 @@ public class AttachmentStreamInfo /// /// Gets or Sets the codec of the attachment. /// - public required string Codec { get; set; } + public string? Codec { get; set; } /// /// Gets or Sets the codec tag of the attachment. diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.Designer.cs new file mode 100644 index 0000000000..d668eea92f --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.Designer.cs @@ -0,0 +1,1657 @@ +// +using System; +using Jellyfin.Database.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20250331182844_FixAttachmentMigration")] + partial class FixAttachmentMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingSubValue") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue") + .IsUnique(); + + b.ToTable("ItemValues"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingScore") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingSubScore") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "UserId", "CustomDataKey"); + + b.HasIndex("UserId"); + + b.HasIndex("ItemId", "UserId", "IsFavorite"); + + b.HasIndex("ItemId", "UserId", "LastPlayedDate"); + + b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("ItemId", "UserId", "Played"); + + b.ToTable("UserData"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Children") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem") + .WithMany("ParentAncestors") + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("UserData") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Navigation("Chapters"); + + b.Navigation("Children"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("ParentAncestors"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs new file mode 100644 index 0000000000..f921856a20 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class FixAttachmentMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Codec", + table: "AttachmentStreamInfos", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Codec", + table: "AttachmentStreamInfos", + type: "TEXT", + nullable: false, + defaultValue: string.Empty, + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs index 0bb4b31b051ac185eca02c29578e437c3e09ff66..08c73217f36c8ce37821db9c69ab4f269d4f3455 100644 GIT binary patch delta 26 icmbPzh574c<_$&ylP?rFO)ki1nJkc>u=$^W>`efs4-4b~ delta 30 mcmex*nR)gV<_$&yllwDbCo8ydO@5k}I{8gO#AXA*qc;HB-3~nf From d1ed6593ad36c360df734c0ad0847d10c9c544be Mon Sep 17 00:00:00 2001 From: timminator <150205162+timminator@users.noreply.github.com> Date: Thu, 3 Apr 2025 17:18:00 +0200 Subject: [PATCH 169/508] Make ReadInputAtNativeFramerate configurable for M3U tuner (#13773) --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 2 +- MediaBrowser.Model/LiveTv/TunerHostInfo.cs | 3 +++ src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 75b3f151d7..99896014f4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -7165,7 +7165,7 @@ namespace MediaBrowser.Controller.MediaEncoding state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders; state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate; - if (state.ReadInputAtNativeFramerate + if ((state.ReadInputAtNativeFramerate && !state.IsSegmentedLiveStream) || (mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))) { diff --git a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs index a355387b1a..b70333bce4 100644 --- a/MediaBrowser.Model/LiveTv/TunerHostInfo.cs +++ b/MediaBrowser.Model/LiveTv/TunerHostInfo.cs @@ -9,6 +9,7 @@ namespace MediaBrowser.Model.LiveTv { AllowHWTranscoding = true; IgnoreDts = true; + ReadAtNativeFramerate = false; AllowStreamSharing = true; AllowFmp4TranscodingContainer = false; FallbackMaxStreamingBitrate = 30000000; @@ -43,5 +44,7 @@ namespace MediaBrowser.Model.LiveTv public string UserAgent { get; set; } public bool IgnoreDts { get; set; } + + public bool ReadAtNativeFramerate { get; set; } } } diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs index be81171a03..fb606be0ef 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs @@ -190,7 +190,7 @@ namespace Jellyfin.LiveTv.TunerHosts RequiresClosing = true, RequiresLooping = info.EnableStreamLooping, - ReadAtNativeFramerate = false, + ReadAtNativeFramerate = info.ReadAtNativeFramerate, Id = channel.Path.GetMD5().ToString("N", CultureInfo.InvariantCulture), IsInfiniteStream = true, From d0c1ef80027fabb4109e8131a950a2468fa9befd Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Thu, 3 Apr 2025 20:42:33 +0200 Subject: [PATCH 170/508] Update MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs Co-authored-by: Cody Robibero --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 17fb42fccc..268549612d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -43,7 +43,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// The level validation regex. /// This regular expression matches strings representing a double. /// - public const string LevelValidationRegex = @"-?\d+(?:\.\d+)?"; + public const string LevelValidationRegex = @"-?[0-9]+(?:\.[0-9]+)?"; private const string _defaultMjpegEncoder = "mjpeg"; From 1c2b48182a5f555be7dcf260139e118f0e716fbd Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Fri, 4 Apr 2025 01:44:47 +0200 Subject: [PATCH 171/508] Fix ArgumentNullException on playlist creation (#13837) mediaSourceId can be null, the IDE doesn't know this as nullable is disabled for BaseEncodingJobOptions --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 4 ++-- .../Playlist/CreateMainPlaylistRequest.cs | 4 ++-- .../Playlist/DynamicHlsPlaylistGenerator.cs | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b501bae4ca..c828533621 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1419,9 +1419,9 @@ public class DynamicHlsController : BaseJellyfinApiController TranscodingJobType, cancellationTokenSource.Token) .ConfigureAwait(false); - + var mediaSourceId = state.BaseRequest.MediaSourceId; var request = new CreateMainPlaylistRequest( - Guid.Parse(state.BaseRequest.MediaSourceId), + mediaSourceId is null ? null : Guid.Parse(mediaSourceId), state.MediaPath, state.SegmentLength * 1000, state.RunTimeTicks ?? 0, diff --git a/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs b/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs index ac9d30b33a..f5af500626 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs +++ b/src/Jellyfin.MediaEncoding.Hls/Playlist/CreateMainPlaylistRequest.cs @@ -18,7 +18,7 @@ public class CreateMainPlaylistRequest /// The URI prefix for the relative URL in the playlist. /// The desired query string to append (must start with ?). /// Whether the video is being remuxed. - public CreateMainPlaylistRequest(Guid mediaSourceId, string filePath, int desiredSegmentLengthMs, long totalRuntimeTicks, string segmentContainer, string endpointPrefix, string queryString, bool isRemuxingVideo) + public CreateMainPlaylistRequest(Guid? mediaSourceId, string filePath, int desiredSegmentLengthMs, long totalRuntimeTicks, string segmentContainer, string endpointPrefix, string queryString, bool isRemuxingVideo) { MediaSourceId = mediaSourceId; FilePath = filePath; @@ -33,7 +33,7 @@ public class CreateMainPlaylistRequest /// /// Gets the media source id. /// - public Guid MediaSourceId { get; } + public Guid? MediaSourceId { get; } /// /// Gets the file path. diff --git a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs index 343f3e562d..fb5027e5b5 100644 --- a/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs +++ b/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs @@ -35,7 +35,9 @@ public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator { IReadOnlyList segments; // For video transcodes it is sufficient with equal length segments as ffmpeg will create new keyframes - if (request.IsRemuxingVideo && TryExtractKeyframes(request.MediaSourceId, request.FilePath, out var keyframeData)) + if (request.IsRemuxingVideo + && request.MediaSourceId is not null + && TryExtractKeyframes(request.MediaSourceId.Value, request.FilePath, out var keyframeData)) { segments = ComputeSegments(keyframeData, request.DesiredSegmentLengthMs); } From f7021d04ebc91eb78bc12ad4fea63ebe34ccc066 Mon Sep 17 00:00:00 2001 From: HigherLevel Date: Thu, 3 Apr 2025 08:28:15 -0400 Subject: [PATCH 172/508] Translated using Weblate (Afrikaans) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/af/ --- Emby.Server.Implementations/Localization/Core/af.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json index e89ede10b4..1dce589234 100644 --- a/Emby.Server.Implementations/Localization/Core/af.json +++ b/Emby.Server.Implementations/Localization/Core/af.json @@ -129,5 +129,11 @@ "TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.", "TaskAudioNormalization": "Odio Normalisering", "TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon", - "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie." + "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie.", + "TaskDownloadMissingLyrics": "Laai tekorte lirieke af", + "TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies", + "TaskExtractMediaSegments": "Media Segment Skandeer", + "TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.", + "TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging", + "TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings." } From 2264d58ae75477595253b53d37560dd930586365 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 5 Apr 2025 15:53:17 +0200 Subject: [PATCH 173/508] Use subdirectories to organize extracted data (#13838) * Use subdirectories to organize extracted data * Apply suggestions from code review --- .../Library/PathManager.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs index ac004b4132..83a6df9644 100644 --- a/Emby.Server.Implementations/Library/PathManager.cs +++ b/Emby.Server.Implementations/Library/PathManager.cs @@ -42,15 +42,17 @@ public class PathManager : IPathManager /// public string GetAttachmentFolderPath(string mediaSourceId) { - var id = Guid.Parse(mediaSourceId); - return Path.Join(AttachmentCachePath, id.ToString("D", CultureInfo.InvariantCulture)); + var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan(); + + return Path.Join(AttachmentCachePath, id[..2], id); } /// public string GetSubtitleFolderPath(string mediaSourceId) { - var id = Guid.Parse(mediaSourceId); - return Path.Join(SubtitleCachePath, id.ToString("D", CultureInfo.InvariantCulture)); + var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan(); + + return Path.Join(SubtitleCachePath, id[..2], id); } /// @@ -62,11 +64,10 @@ public class PathManager : IPathManager /// public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false) { - var basePath = _config.ApplicationPaths.TrickplayPath; - var idString = item.Id.ToString("D", CultureInfo.InvariantCulture); + var id = item.Id.ToString("D", CultureInfo.InvariantCulture).AsSpan(); return saveWithMedia ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) - : Path.Combine(basePath, idString); + : Path.Join(_config.ApplicationPaths.TrickplayPath, id[..2], id); } } From 0e7ae0e9a4865fbd2c3c11a91207e106d6d0b368 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 5 Apr 2025 17:57:58 +0200 Subject: [PATCH 174/508] Fix indices and update of ItemValues (#13843) --- .../Item/BaseItemRepository.cs | 2 +- .../ItemValuesConfiguration.cs | 3 +- ...405075612_FixItemValuesIndices.Designer.cs | 1694 +++++++++++++++++ .../20250405075612_FixItemValuesIndices.cs | 47 + .../Migrations/JellyfinDbModelSnapshot.cs | Bin 59893 -> 59943 bytes 5 files changed, 1744 insertions(+), 2 deletions(-) create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.Designer.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index c7cd54ed18..3e5dd1a5bd 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -535,7 +535,7 @@ public sealed class BaseItemRepository if (!localItemValueCache.TryGetValue(itemValue, out var refValue)) { refValue = context.ItemValues - .Where(f => f.CleanValue == GetCleanValue(itemValue.Value) && (int)f.Type == itemValue.MagicNumber) + .Where(f => f.Value == itemValue.Value && (int)f.Type == itemValue.MagicNumber) .Select(e => e.ItemValueId) .FirstOrDefault(); } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs index c8e003eaa1..97ebc2e01b 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs @@ -13,6 +13,7 @@ public class ItemValuesConfiguration : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { builder.HasKey(e => e.ItemValueId); - builder.HasIndex(e => new { e.Type, e.CleanValue }).IsUnique(); + builder.HasIndex(e => new { e.Type, e.CleanValue }); + builder.HasIndex(e => new { e.Type, e.Value }).IsUnique(); } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.Designer.cs new file mode 100644 index 0000000000..4ba3352edc --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.Designer.cs @@ -0,0 +1,1694 @@ +// +using System; +using Jellyfin.Database.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20250405075612_FixItemValuesIndices")] + partial class FixItemValuesIndices + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateCreatedFilesystem") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingSubValue") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue"); + + b.HasIndex("Type", "Value") + .IsUnique(); + + b.ToTable("ItemValues"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("KeyframeTicks") + .HasColumnType("TEXT"); + + b.Property("TotalDuration") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId"); + + b.ToTable("KeyframeData"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Hdr10PlusPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingScore") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingSubScore") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "UserId", "CustomDataKey"); + + b.HasIndex("UserId"); + + b.HasIndex("ItemId", "UserId", "IsFavorite"); + + b.HasIndex("ItemId", "UserId", "LastPlayedDate"); + + b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("ItemId", "UserId", "Played"); + + b.ToTable("UserData"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Parents") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem") + .WithMany("Children") + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("UserData") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Navigation("Chapters"); + + b.Navigation("Children"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("Parents"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs new file mode 100644 index 0000000000..aa667bafd4 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class FixItemValuesIndices : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_ItemValues_Type_CleanValue", + table: "ItemValues"); + + migrationBuilder.CreateIndex( + name: "IX_ItemValues_Type_CleanValue", + table: "ItemValues", + columns: new[] { "Type", "CleanValue" }); + + migrationBuilder.CreateIndex( + name: "IX_ItemValues_Type_Value", + table: "ItemValues", + columns: new[] { "Type", "Value" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_ItemValues_Type_CleanValue", + table: "ItemValues"); + + migrationBuilder.DropIndex( + name: "IX_ItemValues_Type_Value", + table: "ItemValues"); + + migrationBuilder.CreateIndex( + name: "IX_ItemValues_Type_CleanValue", + table: "ItemValues", + columns: new[] { "Type", "CleanValue" }, + unique: true); + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs index 08c73217f36c8ce37821db9c69ab4f269d4f3455..34c83ad361159df5d48d16deaf7faa03e367ad18 100644 GIT binary patch delta 30 mcmex*nR)pY<_$WDlS}H9C+it=OjhtvoNU*}wK*VhX$JtxjtoHn delta 30 mcmZ2}h574c<_$WDle^<(Ca>${nEcd3Y4X`*uFdNbD?0$=V-92h From de3d1445c06a9ca485079deee3d09e427b8b4402 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 5 Apr 2025 18:49:29 +0200 Subject: [PATCH 175/508] Fix ancestors (#13827) --- .../Item/BaseItemRepository.cs | 6 +- .../Entities/BaseItemEntity.cs | 2 +- .../AncestorIdConfiguration.cs | 4 +- .../BaseItemConfiguration.cs | 2 +- .../20250401142247_FixAncestors.Designer.cs | 1658 +++++++++++++++++ .../Migrations/20250401142247_FixAncestors.cs | 20 + .../Migrations/JellyfinDbModelSnapshot.cs | Bin 59943 -> 59927 bytes 7 files changed, 1685 insertions(+), 7 deletions(-) create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.Designer.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 3e5dd1a5bd..4a6a7f8cdc 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -2090,7 +2090,7 @@ public sealed class BaseItemRepository if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey)) { baseQuery = baseQuery - .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.ParentAncestors!.Any(w => w.ItemId == f.Id))); + .Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.Children!.Any(w => w.ItemId == e.Id))); } if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey)) @@ -2125,7 +2125,7 @@ public sealed class BaseItemRepository { baseQuery = baseQuery .Where(e => - e.ParentAncestors! + e.Parents! .Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue)) || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""))); @@ -2134,7 +2134,7 @@ public sealed class BaseItemRepository else { baseQuery = baseQuery - .Where(e => e.ParentAncestors!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue)))); + .Where(e => e.Parents!.Any(f => f.ParentItem.ItemValues!.Any(w => w.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(w.ItemValue.CleanValue)))); } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs index 332dec2e67..a09a96317c 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs @@ -164,7 +164,7 @@ public class BaseItemEntity public ICollection? Provider { get; set; } - public ICollection? ParentAncestors { get; set; } + public ICollection? Parents { get; set; } public ICollection? Children { get; set; } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs index 1cb4a1eb1d..67269153d3 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs @@ -14,7 +14,7 @@ public class AncestorIdConfiguration : IEntityTypeConfiguration { builder.HasKey(e => new { e.ItemId, e.ParentItemId }); builder.HasIndex(e => e.ParentItemId); - builder.HasOne(e => e.ParentItem).WithMany(e => e.ParentAncestors).HasForeignKey(f => f.ParentItemId); - builder.HasOne(e => e.Item).WithMany(e => e.Children).HasForeignKey(f => f.ItemId); + builder.HasOne(e => e.ParentItem).WithMany(e => e.Children).HasForeignKey(f => f.ParentItemId); + builder.HasOne(e => e.Item).WithMany(e => e.Parents).HasForeignKey(f => f.ItemId); } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs index 37816faece..4a76113bf6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs @@ -24,7 +24,7 @@ public class BaseItemConfiguration : IEntityTypeConfiguration builder.HasMany(e => e.MediaStreams); builder.HasMany(e => e.Chapters); builder.HasMany(e => e.Provider); - builder.HasMany(e => e.ParentAncestors); + builder.HasMany(e => e.Parents); builder.HasMany(e => e.Children); builder.HasMany(e => e.LockedFields); builder.HasMany(e => e.TrailerTypes); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.Designer.cs new file mode 100644 index 0000000000..d7672b1379 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.Designer.cs @@ -0,0 +1,1658 @@ +// +using System; +using Jellyfin.Database.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDbContext))] + [Migration("20250401142247_FixAncestors")] + partial class FixAncestors + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingSubValue") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.HasIndex("PresentationUniqueKey"); + + b.HasIndex("TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "Id"); + + b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); + + b.HasIndex("Type", "TopParentId", "StartDate"); + + b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem"); + + b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); + + b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); + + b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); + + b.ToTable("BaseItems"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue") + .IsUnique(); + + b.ToTable("ItemValues"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.HasIndex("StreamIndex"); + + b.HasIndex("StreamType"); + + b.HasIndex("StreamIndex", "StreamType"); + + b.HasIndex("StreamIndex", "StreamType", "Language"); + + b.ToTable("MediaStreamInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.HasIndex("ItemId", "ListOrder"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("PeopleBaseItemMap"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("AccessToken", "DateLastActivity"); + + b.HasIndex("DeviceId", "DateLastActivity"); + + b.HasIndex("UserId", "DeviceId"); + + b.ToTable("Devices"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingScore") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalRatingSubScore") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("SubtitleStreamIndex") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "UserId", "CustomDataKey"); + + b.HasIndex("UserId"); + + b.HasIndex("ItemId", "UserId", "IsFavorite"); + + b.HasIndex("ItemId", "UserId", "LastPlayedDate"); + + b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); + + b.HasIndex("ItemId", "UserId", "Played"); + + b.ToTable("UserData"); + + b.HasAnnotation("Sqlite:UseSqlReturningClause", false); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("AccessSchedules") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Parents") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem") + .WithMany("Children") + .HasForeignKey("ParentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ParentItem"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Images") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("LockedFields") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Provider") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("TrailerTypes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Chapters") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("DisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null) + .WithMany("HomeSections") + .HasForeignKey("DisplayPreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithOne("ProfileImage") + .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("ItemDisplayPreferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("ItemValues") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue") + .WithMany("BaseItemsMap") + .HasForeignKey("ItemValueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("ItemValue"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("MediaStreams") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("Peoples") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People") + .WithMany("BaseItems") + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("People"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) + .WithMany("Preferences") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => + { + b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") + .WithMany("UserData") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => + { + b.Navigation("Chapters"); + + b.Navigation("Children"); + + b.Navigation("Images"); + + b.Navigation("ItemValues"); + + b.Navigation("LockedFields"); + + b.Navigation("MediaStreams"); + + b.Navigation("Parents"); + + b.Navigation("Peoples"); + + b.Navigation("Provider"); + + b.Navigation("TrailerTypes"); + + b.Navigation("UserData"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => + { + b.Navigation("HomeSections"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => + { + b.Navigation("BaseItemsMap"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => + { + b.Navigation("BaseItems"); + }); + + modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => + { + b.Navigation("AccessSchedules"); + + b.Navigation("DisplayPreferences"); + + b.Navigation("ItemDisplayPreferences"); + + b.Navigation("Permissions"); + + b.Navigation("Preferences"); + + b.Navigation("ProfileImage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs new file mode 100644 index 0000000000..e1220bfcf7 --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// + public partial class FixAncestors : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/JellyfinDbModelSnapshot.cs index 34c83ad361159df5d48d16deaf7faa03e367ad18..dcdc5dd3eccba703c7955c6e45330bba7455b205 100644 GIT binary patch delta 55 zcmV-70LcHR)B~5)1F)98lfS+OlTl?Lld##qlb|&SlTXA7laIbIla{?Rv-!R?CX*q5 N9Ft(_4zpn8#{>`18&v=R delta 82 zcmbP!g?afE<_**KPTsfAc=Da=jgt#b8&6(f$}#!hajwaI`waL35{pvvN*wc&Q;SRT kizWxADonm`ooDmMeWq&cP|?W;?PMo6+!op#^7JqZ0HCcWGynhq From fe79384cd50584ffd55f494c8e88df59ca5acdc1 Mon Sep 17 00:00:00 2001 From: JQ <81431263+scampower3@users.noreply.github.com> Date: Mon, 7 Apr 2025 04:18:39 +0800 Subject: [PATCH 176/508] Returns album artists apart from artist names when doing a lyrics search (#13852) --- MediaBrowser.Model/Lyrics/LyricSearchRequest.cs | 7 ++++++- MediaBrowser.Providers/Lyric/LyricManager.cs | 3 ++- MediaBrowser.Providers/Lyric/LyricScheduledTask.cs | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Model/Lyrics/LyricSearchRequest.cs b/MediaBrowser.Model/Lyrics/LyricSearchRequest.cs index 48c442a55e..67f3d7b42c 100644 --- a/MediaBrowser.Model/Lyrics/LyricSearchRequest.cs +++ b/MediaBrowser.Model/Lyrics/LyricSearchRequest.cs @@ -15,7 +15,12 @@ public class LyricSearchRequest : IHasProviderIds public string? MediaPath { get; set; } /// - /// Gets or sets the artist name. + /// Gets or sets the album artist names. + /// + public IReadOnlyList? AlbumArtistsNames { get; set; } + + /// + /// Gets or sets the artist names. /// public IReadOnlyList? ArtistNames { get; set; } diff --git a/MediaBrowser.Providers/Lyric/LyricManager.cs b/MediaBrowser.Providers/Lyric/LyricManager.cs index f4b18a8c14..913a104a0d 100644 --- a/MediaBrowser.Providers/Lyric/LyricManager.cs +++ b/MediaBrowser.Providers/Lyric/LyricManager.cs @@ -78,7 +78,8 @@ public class LyricManager : ILyricManager MediaPath = audio.Path, SongName = audio.Name, AlbumName = audio.Album, - ArtistNames = audio.GetAllArtists().ToList(), + AlbumArtistsNames = audio.AlbumArtists, + ArtistNames = audio.Artists, Duration = audio.RunTimeTicks, IsAutomated = isAutomated }; diff --git a/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs b/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs index 73912b5796..b8861ee5ec 100644 --- a/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs +++ b/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs @@ -117,7 +117,8 @@ public class LyricScheduledTask : IScheduledTask MediaPath = audioItem.Path, SongName = audioItem.Name, AlbumName = audioItem.Album, - ArtistNames = audioItem.GetAllArtists().ToList(), + AlbumArtistsNames = audioItem.AlbumArtists, + ArtistNames = audioItem.Artists, Duration = audioItem.RunTimeTicks, IsAutomated = true, DisabledLyricFetchers = libraryOptions.DisabledLyricFetchers, From b65e03da9ad0a09be8025e9ee10d96c3e082b4bb Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Mon, 7 Apr 2025 14:11:05 +0200 Subject: [PATCH 177/508] Fix Genre type (#13862) --- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 4a6a7f8cdc..f3e7bb3c58 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -68,7 +68,7 @@ public sealed class BaseItemRepository private static readonly IReadOnlyList _getArtistValueTypes = [ItemValueType.Artist]; private static readonly IReadOnlyList _getAlbumArtistValueTypes = [ItemValueType.AlbumArtist]; private static readonly IReadOnlyList _getStudiosValueTypes = [ItemValueType.Studios]; - private static readonly IReadOnlyList _getGenreValueTypes = [ItemValueType.Studios]; + private static readonly IReadOnlyList _getGenreValueTypes = [ItemValueType.Genre]; /// /// Initializes a new instance of the class. From 1ef0a4106649c4fbb252840466ffb5584322eb55 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:11:49 +0000 Subject: [PATCH 178/508] Update github/codeql-action action to v3.28.14 --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 9faab47db9..44f31012a5 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 + uses: github/codeql-action/init@fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2 # v3.28.14 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 + uses: github/codeql-action/autobuild@fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2 # v3.28.14 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 + uses: github/codeql-action/analyze@fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2 # v3.28.14 From 04ca27ad075bdab1eae49c43fde1ed6d45c050e4 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 7 Apr 2025 16:59:00 +0200 Subject: [PATCH 179/508] Fix backup not written to correct directory (#13853) * Fix backup not written to correct directory * Improve restore handling and only restore on actual error * Fix first failed migration not causing a rollback --- Jellyfin.Server/Migrations/MigrationRunner.cs | 33 +++++++++++++++---- .../SqliteDatabaseProvider.cs | 4 ++- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index c3a2e1bc4e..c223576dad 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -149,11 +149,18 @@ namespace Jellyfin.Server.Migrations } } + List databaseMigrations = []; try { foreach (var migrationRoutine in migrationsToBeApplied) { logger.LogInformation("Applying migration '{Name}'", migrationRoutine.Name); + var isDbMigration = migrationRoutine is IDatabaseMigrationRoutine; + + if (isDbMigration) + { + databaseMigrations.Add(migrationRoutine); + } try { @@ -167,17 +174,31 @@ namespace Jellyfin.Server.Migrations // Mark the migration as completed logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name); - migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name)); - saveConfiguration(migrationOptions); - logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name); + if (!isDbMigration) + { + migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name)); + saveConfiguration(migrationOptions); + logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name); + } } } - catch (System.Exception) when (migrationKey is not null && jellyfinDatabaseProvider is not null) + catch (Exception) when (migrationKey is not null && jellyfinDatabaseProvider is not null) { - logger.LogInformation("Rollback on database as migration reported failure."); - await jellyfinDatabaseProvider.RestoreBackupFast(migrationKey, CancellationToken.None).ConfigureAwait(false); + if (databaseMigrations.Count != 0) + { + logger.LogInformation("Rolling back database as migrations reported failure."); + await jellyfinDatabaseProvider.RestoreBackupFast(migrationKey, CancellationToken.None).ConfigureAwait(false); + } + throw; } + + foreach (var migrationRoutine in databaseMigrations) + { + migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name)); + saveConfiguration(migrationOptions); + logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name); + } } } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index e818c3524a..bd5631074f 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -98,7 +98,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider Directory.CreateDirectory(backupFile); } - backupFile = Path.Combine(_applicationPaths.DataPath, $"{key}_jellyfin.db"); + backupFile = Path.Combine(backupFile, $"{key}_jellyfin.db"); File.Copy(path, backupFile); return Task.FromResult(key); } @@ -106,6 +106,8 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider /// public Task RestoreBackupFast(string key, CancellationToken cancellationToken) { + // ensure there are absolutly no dangling Sqlite connections. + SqliteConnection.ClearAllPools(); var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db"); var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName, $"{key}_jellyfin.db"); From 82a561b87d3b6d023168d4604c1030ff936f55f4 Mon Sep 17 00:00:00 2001 From: Alex <48403821+AlexDalas@users.noreply.github.com> Date: Tue, 8 Apr 2025 00:59:18 +1000 Subject: [PATCH 180/508] Add API support for ELRC word-based lyrics (#12941) * Add API support for ELRC word-based lyrics Adds support for word-based timestamps from within ELRC files. * Create TimeTags object * redo TimeTag implementation Change TimeTag to long, redo TimeTag implementation Make timestamp not nullable Update MediaBrowser.Model/Lyrics/LyricLine.cs Make TimeTag list IReadOnlyList Remove nullable Timestamp Update TimeTag description Co-Authored-By: Cody Robibero * Changes to LyricLineTimeTag Moved TimeTag to LyricLineTimeTag Change "timestamp" to "start" for consistency Change plural "TimeTags" to "Cues" Change comments * Change LyricLineTimeTag to LyricLineCue, include info about end times * Remove width * Remove width tag * Rewrite cue parser and add tests --------- Co-authored-by: Cody Robibero --- MediaBrowser.Model/Lyrics/LyricLine.cs | 11 ++++- MediaBrowser.Model/Lyrics/LyricLineCue.cs | 35 +++++++++++++++ .../Lyric/LrcLyricParser.cs | 45 ++++++++++++++++--- .../Lyrics/LrcLyricParserTests.cs | 41 +++++++++++++++++ .../Lyrics/Fleetwood Mac - Rumors.elrc | 31 +++++++++++++ 5 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 MediaBrowser.Model/Lyrics/LyricLineCue.cs create mode 100644 tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs create mode 100644 tests/Jellyfin.Providers.Tests/Test Data/Lyrics/Fleetwood Mac - Rumors.elrc diff --git a/MediaBrowser.Model/Lyrics/LyricLine.cs b/MediaBrowser.Model/Lyrics/LyricLine.cs index 64d1f64c2c..788bace69a 100644 --- a/MediaBrowser.Model/Lyrics/LyricLine.cs +++ b/MediaBrowser.Model/Lyrics/LyricLine.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace MediaBrowser.Model.Lyrics; /// @@ -10,10 +12,12 @@ public class LyricLine /// /// The lyric text. /// The lyric start time in ticks. - public LyricLine(string text, long? start = null) + /// The time-aligned cues for the song's lyrics. + public LyricLine(string text, long? start = null, IReadOnlyList? cues = null) { Text = text; Start = start; + Cues = cues; } /// @@ -25,4 +29,9 @@ public class LyricLine /// Gets the start time in ticks. /// public long? Start { get; } + + /// + /// Gets the time-aligned cues for the song's lyrics. + /// + public IReadOnlyList? Cues { get; } } diff --git a/MediaBrowser.Model/Lyrics/LyricLineCue.cs b/MediaBrowser.Model/Lyrics/LyricLineCue.cs new file mode 100644 index 0000000000..1172a0231d --- /dev/null +++ b/MediaBrowser.Model/Lyrics/LyricLineCue.cs @@ -0,0 +1,35 @@ +namespace MediaBrowser.Model.Lyrics; + +/// +/// LyricLineCue model, holds information about the timing of words within a LyricLine. +/// +public class LyricLineCue +{ + /// + /// Initializes a new instance of the class. + /// + /// The start of the character index of the lyric. + /// The start of the timestamp the lyric is synced to in ticks. + /// The end of the timestamp the lyric is synced to in ticks. + public LyricLineCue(int position, long start, long? end) + { + Position = position; + Start = start; + End = end; + } + + /// + /// Gets the character index of the lyric. + /// + public int Position { get; } + + /// + /// Gets the timestamp the lyric is synced to in ticks. + /// + public long Start { get; } + + /// + /// Gets the end timestamp the lyric is synced to in ticks. + /// + public long? End { get; } +} diff --git a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs index fffdf4887b..27d17b535c 100644 --- a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs +++ b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using Jellyfin.Extensions; using LrcParser.Model; using LrcParser.Parser; @@ -14,7 +15,7 @@ namespace MediaBrowser.Providers.Lyric; /// /// LRC Lyric Parser. /// -public class LrcLyricParser : ILyricParser +public partial class LrcLyricParser : ILyricParser { private readonly LyricParser _lrcLyricParser; @@ -65,13 +66,47 @@ public class LrcLyricParser : ILyricParser } List lyricList = []; - - for (int i = 0; i < sortedLyricData.Count; i++) + for (var l = 0; l < sortedLyricData.Count; l++) { - long ticks = TimeSpan.FromMilliseconds(sortedLyricData[i].StartTime).Ticks; - lyricList.Add(new LyricLine(sortedLyricData[i].Text.Trim(), ticks)); + var cues = new List(); + var lyric = sortedLyricData[l]; + + if (lyric.TimeTags.Count != 0) + { + var keys = lyric.TimeTags.Keys.ToList(); + int current = 0, next = 1; + while (next < keys.Count) + { + var currentKey = keys[current]; + var currentMs = lyric.TimeTags[currentKey] ?? 0; + var nextMs = lyric.TimeTags[keys[next]] ?? 0; + + cues.Add(new LyricLineCue( + position: Math.Max(currentKey.Index, 0), + start: TimeSpan.FromMilliseconds(currentMs).Ticks, + end: TimeSpan.FromMilliseconds(nextMs).Ticks)); + + current++; + next++; + } + + var lastKey = keys[current]; + var lastMs = lyric.TimeTags[lastKey] ?? 0; + + cues.Add(new LyricLineCue( + position: Math.Max(lastKey.Index, 0), + start: TimeSpan.FromMilliseconds(lastMs).Ticks, + end: l + 1 < sortedLyricData.Count ? TimeSpan.FromMilliseconds(sortedLyricData[l + 1].StartTime).Ticks : null)); + } + + long lyricStartTicks = TimeSpan.FromMilliseconds(lyric.StartTime).Ticks; + lyricList.Add(new LyricLine(WhitespaceRegex().Replace(lyric.Text.Trim(), " "), lyricStartTicks, cues)); } return new LyricDto { Lyrics = lyricList }; } + + // Replacement is required until https://github.com/karaoke-dev/LrcParser/issues/83 is resolved. + [GeneratedRegex(@"\s+")] + private static partial Regex WhitespaceRegex(); } diff --git a/tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs b/tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs new file mode 100644 index 0000000000..756a688abe --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/Lyrics/LrcLyricParserTests.cs @@ -0,0 +1,41 @@ +using System.IO; +using MediaBrowser.Model.Lyrics; +using MediaBrowser.Providers.Lyric; +using Xunit; + +namespace Jellyfin.Providers.Tests.Lyrics; + +public static class LrcLyricParserTests +{ + [Fact] + public static void ParseElrcCues() + { + var parser = new LrcLyricParser(); + var fileContents = File.ReadAllText(Path.Combine("Test Data", "Lyrics", "Fleetwood Mac - Rumors.elrc")); + var parsed = parser.ParseLyrics(new LyricFile("Fleetwood Mac - Rumors.elrc", fileContents)); + + Assert.NotNull(parsed); + Assert.Equal(31, parsed.Lyrics.Count); + + var line1 = parsed.Lyrics[0]; + Assert.Equal("Every night that goes between", line1.Text); + Assert.NotNull(line1.Cues); + Assert.Equal(9, line1.Cues.Count); + Assert.Equal(68400000, line1.Cues[0].Start); + Assert.Equal(72000000, line1.Cues[0].End); + + var line5 = parsed.Lyrics[4]; + Assert.Equal("Every night you do not come", line5.Text); + Assert.NotNull(line5.Cues); + Assert.Equal(11, line5.Cues.Count); + Assert.Equal(377300000, line5.Cues[5].Start); + Assert.Equal(380000000, line5.Cues[5].End); + + var lastLine = parsed.Lyrics[^1]; + Assert.Equal("I have always been a storm", lastLine.Text); + Assert.NotNull(lastLine.Cues); + Assert.Equal(11, lastLine.Cues.Count); + Assert.Equal(2358000000, lastLine.Cues[^1].Start); + Assert.Null(lastLine.Cues[^1].End); + } +} diff --git a/tests/Jellyfin.Providers.Tests/Test Data/Lyrics/Fleetwood Mac - Rumors.elrc b/tests/Jellyfin.Providers.Tests/Test Data/Lyrics/Fleetwood Mac - Rumors.elrc new file mode 100644 index 0000000000..1df264c903 --- /dev/null +++ b/tests/Jellyfin.Providers.Tests/Test Data/Lyrics/Fleetwood Mac - Rumors.elrc @@ -0,0 +1,31 @@ +[00:06.84] <00:06.84> Every <00:07.20> <00:07.56> night <00:07.87> <00:08.19> that <00:08.46> <00:08.79> goes <00:09.19> <00:09.59> between +[00:14.69] <00:14.69> I <00:14.78> <00:14.87> feel <00:15.15> <00:15.44> a <00:15.54> <00:15.65> little <00:15.96> <00:16.28> less +[00:20.98] <00:20.98> As <00:21.10> <00:21.22> you <00:21.27> <00:21.31> slowly <00:21.79> <00:22.27> go <00:22.57> <00:22.87> away <00:23.74> <00:24.19> from <00:24.51> <00:24.82> me +[00:28.73] <00:28.73> This <00:28.91> <00:29.09> is <00:29.22> <00:29.36> only <00:29.75> <00:30.14> another <00:30.92> <00:31.25> test +[00:36.11] <00:36.11> Every <00:36.44> <00:36.77> night <00:37.14> <00:37.52> you <00:37.73> <00:38.00> do <00:38.24> <00:38.48> not <00:38.93> <00:39.41> come +[00:43.56] <00:43.56> Your <00:43.65> <00:43.74> softness <00:44.21> <00:44.69> fades <00:45.01> <00:45.33> away +[00:50.29] <00:50.29> Did <00:50.42> <00:50.56> I <00:50.70> <00:50.85> ever <00:51.41> <00:51.97> really <00:52.48> <00:52.99> care <00:53.56> <00:53.86> that <00:54.09> <00:54.34> much +[00:58.07] <00:58.07> Is <00:58.20> <00:58.34> there <00:58.48> <00:58.63> anything <00:59.44> <00:59.69> left <00:59.94> <01:00.20> to <01:00.30> <01:00.41> say? +[01:05.59] <01:05.59> Every <01:06.58> <01:07.78> hour <01:08.05> <01:08.32> of <01:08.92> <01:09.97> fear <01:10.39> <01:10.81> I <01:11.41> <01:11.47> spend +[01:13.84] <01:13.84> My <01:13.99> <01:14.14> body <01:14.57> <01:15.01> tries <01:15.32> <01:15.64> to <01:15.71> <01:15.79> cry +[01:18.60] <01:18.60> Living <01:19.41> <01:20.79> through <01:21.12> <01:21.45> each <01:21.90> <01:23.13> empty <01:23.83> <01:24.54> night +[01:27.51] <01:27.51> A <01:27.60> <01:27.69> deadly <01:28.12> <01:28.56> call <01:28.90> <01:29.25> inside +[01:34.29] <01:34.29> I <01:34.39> <01:34.50> haven't <01:35.37> <01:35.64> felt <01:35.92> <01:36.21> this <01:36.34> <01:36.48> way <01:36.79> <01:37.11> I <01:37.35> <01:37.62> feel +[01:42.11] <01:42.11> Since <01:42.27> <01:42.44> many <01:42.71> <01:42.98> a <01:43.08> <01:43.19> years <01:43.46> <01:43.85> ago +[01:48.92] <01:48.92> But <01:49.08> <01:49.25> in <01:49.36> <01:49.48> those <01:49.78> <01:50.09> years <01:50.50> <01:50.92> and <01:51.07> <01:51.23> the <01:51.30> <01:51.38> lifetime's <01:51.99> <01:52.61> past +[01:56.60] <01:56.60> I <01:56.69> <01:56.78> did <01:56.91> <01:57.05> not <01:57.50> <01:57.92> deal <01:58.22> <01:58.52> with <01:58.68> <01:58.85> the <01:58.91> <01:58.97> road +[02:03.13] <02:03.13> And <02:03.25> <02:03.37> I <02:03.45> <02:03.55> did <02:03.68> <02:03.81> not <02:04.20> <02:04.60> deal <02:04.94> <02:05.29> with <02:05.45> <02:05.62> you, <02:05.90> <02:06.19> I <02:06.50> <02:06.82> know +[02:10.95] <02:10.95> Though <02:11.11> <02:11.28> the <02:11.35> <02:11.43> love <02:11.79> <02:12.15> has <02:12.32> <02:12.39> always <02:13.06> <02:13.74> been +[02:17.70] <02:17.70> So <02:17.91> <02:18.12> I <02:18.16> <02:18.21> search <02:18.55> <02:18.90> to <02:19.03> <02:19.17> find <02:19.51> <02:19.86> an <02:20.14> <02:20.43> answer <02:21.24> <02:21.71> there +[02:25.72] <02:25.72> So <02:25.96> <02:26.20> I <02:26.39> <02:26.59> can <02:27.04> <02:27.19> truly <02:27.65> <02:28.12> win +[02:33.02] <02:33.02> Every <02:34.04> <02:35.29> hour <02:35.77> <02:35.84> of <02:36.38> <02:37.43> fear <02:37.92> <02:38.42> I <02:38.69> <02:38.96> spend +[02:41.35] <02:41.35> My <02:41.63> <02:41.91> body <02:42.35> <02:42.79> tries <02:43.09> <02:43.39> to <02:43.48> <02:43.57> cry +[02:46.32] <02:46.32> Living <02:47.04> <02:48.54> through <02:48.88> <02:49.23> each <02:49.56> <02:50.85> empty <02:51.55> <02:52.26> night +[02:55.06] <02:55.06> A <02:55.13> <02:55.21> deadly <02:55.64> <02:56.08> call <02:56.42> <02:56.77> inside +[03:01.50] <03:01.50> So <03:01.74> <03:01.98> I <03:02.04> <03:02.10> try <03:02.42> <03:02.76> to <03:02.81> <03:02.87> say <03:03.45> <03:04.20> goodbye, <03:04.53> <03:04.86> my <03:05.02> <03:05.19> friend +[03:09.09] <03:09.09> I'd <03:09.19> <03:09.30> like <03:09.42> <03:09.54> to <03:09.70> <03:09.87> leave <03:10.02> <03:10.17> you <03:10.29> <03:10.41> with <03:10.71> <03:10.89> something <03:11.32> <03:11.76> warm +[03:16.25] <03:16.25> But <03:16.34> <03:16.43> never <03:16.77> <03:17.12> have <03:17.25> <03:17.38> I <03:17.44> <03:17.51> been <03:17.73> <03:17.96> a <03:18.14> <03:18.32> blue <03:18.62> <03:18.92> calm <03:19.58> <03:19.76> sea +[03:24.18] <03:24.18> I <03:24.31> <03:24.45> have <03:24.57> <03:24.69> always <03:25.59> <03:28.02> been <03:28.19> <03:28.38> a <03:28.51> <03:28.65> storm +[03:33.69] <03:33.69> Always <03:35.43> <03:36.99> been <03:37.18> <03:37.38> a <03:37.50> <03:37.62> storm +[03:41.45] <03:41.45> Ooh, <03:41.99> <03:42.53> always <03:44.42> <03:45.86> been <03:46.05> <03:46.25> a <03:46.41> <03:46.58> storm +[03:50.34] <03:50.34> I <03:50.49> <03:50.64> have <03:51.07> <03:51.51> always <03:53.31> <03:54.78> been <03:54.96> <03:55.14> a <03:55.47> <03:55.80> storm From 77ad7f6139e4911168e2199fe48e78bf7fdddbf1 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 7 Apr 2025 22:42:01 +0200 Subject: [PATCH 181/508] Fix the migration as the new constraint now uses Value as unique key (#13867) --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 3fc9bea842..105fd555f6 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -138,14 +138,14 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine """; // EFCores local lookup sucks. We cannot use context.ItemValues.Local here because its just super slow. - var localItems = new Dictionary<(int Type, string CleanValue), (Database.Implementations.Entities.ItemValue ItemValue, List ItemIds)>(); + var localItems = new Dictionary<(int Type, string Value), (Database.Implementations.Entities.ItemValue ItemValue, List ItemIds)>(); using (new TrackedMigrationStep("loading ItemValues", _logger)) { foreach (SqliteDataReader dto in connection.Query(itemValueQuery)) { var itemId = dto.GetGuid(0); var entity = GetItemValue(dto); - var key = ((int)entity.Type, entity.CleanValue); + var key = ((int)entity.Type, entity.Value); if (!localItems.TryGetValue(key, out var existing)) { localItems[key] = existing = (entity, []); From 4fa1a9cb9703ba88ea714b3540dd4c2a46979900 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 01:06:31 +0000 Subject: [PATCH 182/508] Update github/codeql-action action to v3.28.15 --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 44f31012a5..1eced1913b 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2 # v3.28.14 + uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2 # v3.28.14 + uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2 # v3.28.14 + uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 From 5fc1b1c862f30f81010c3c87dbb753c6f0e4dcfb Mon Sep 17 00:00:00 2001 From: baka0815 Date: Tue, 8 Apr 2025 05:29:12 +0200 Subject: [PATCH 183/508] Translate the ISO-639-2/B codes to ISO-639-2/T. (#13068) * Translate the ISO-639-2/B codes to ISO-639-2/T. This enables 19 additional languages to be displayed correctly. * Convert the 2-dimensional array to a dictionary * Added the French language to the list of ISO-639-2/B codes * Don't change the property, use a local variable instead. * When creating the MediaStream in the MediaStreamRepository ensure that the ISO 639-2/T (f.e. deu) code is used for the language as that is the one the .NET culture info knows. The other code is most likely the ISO 639-2/B code (f.e. ger) which is unknown to the .NET culture info and will result in just displaying the code instead of the display name. * Move the substitution of ISO 639-2/B to /T to the localization manager. Some language (like Chinese) have multiple entries in the iso6392.txt file (f.e. zho|chi|zh|..., zho|chi|zh-tw|...) but the conversation between /T and /B is the same so use .TryAdd. * Change the method definition from GetISO6392TFromB to TryGetISO6392TFromB and return true if a case was found. * Add unit tests for TryGetISO6392TFromB. --- .../Localization/LocalizationManager.cs | 31 +++++++++++++++++++ .../Item/MediaStreamRepository.cs | 13 +++++++- MediaBrowser.Model/Entities/MediaStream.cs | 5 +-- .../Globalization/ILocalizationManager.cs | 9 ++++++ .../Localization/LocalizationManagerTests.cs | 25 +++++++++++++++ 5 files changed, 80 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 9598f9e6c7..17db7ad4c4 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Concurrent; +using System.Collections.Frozen; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -38,6 +40,8 @@ namespace Emby.Server.Implementations.Localization private List _cultures = []; + private FrozenDictionary _iso6392BtoT = null!; + /// /// Initializes a new instance of the class. /// @@ -100,6 +104,7 @@ namespace Emby.Server.Implementations.Localization private async Task LoadCultures() { List list = []; + Dictionary iso6392BtoTdict = new Dictionary(); using var stream = _assembly.GetManifestResourceStream(CulturesPath); if (stream is null) @@ -142,12 +147,17 @@ namespace Emby.Server.Implementations.Localization else { threeLetterNames = [parts[0], parts[1]]; + + // In cases where there are two TLN the first one is ISO 639-2/T and the second one is ISO 639-2/B + // We need ISO 639-2/T for the .NET cultures so we cultivate a dictionary for the translation B->T + iso6392BtoTdict.TryAdd(parts[1], parts[0]); } list.Add(new CultureDto(name, name, twoCharName, threeLetterNames)); } _cultures = list; + _iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); } } @@ -505,5 +515,26 @@ namespace Emby.Server.Implementations.Localization yield return new LocalizationOption("漢語 (繁體字)", "zh-TW"); yield return new LocalizationOption("廣東話 (香港)", "zh-HK"); } + + /// + public bool TryGetISO6392TFromB(string isoB, [NotNullWhen(true)] out string? isoT) + { + // Unlikely case the dictionary is not (yet) initialized properly + if (_iso6392BtoT == null) + { + isoT = null; + return false; + } + + var result = _iso6392BtoT.TryGetValue(isoB, out isoT) && !string.IsNullOrEmpty(isoT); + + // Ensure the ISO code being null if the result is false + if (!result) + { + isoT = null; + } + + return result; + } } } diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs index 1be31db72b..7eb13b7408 100644 --- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs +++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs @@ -100,7 +100,18 @@ public class MediaStreamRepository : IMediaStreamRepository dto.IsAVC = entity.IsAvc; dto.Codec = entity.Codec; - dto.Language = entity.Language; + + var language = entity.Language; + + // Check if the language has multiple three letter ISO codes + // if yes choose the first as that is the ISO 639-2/T code we're needing + if (language != null && _localization.TryGetISO6392TFromB(language, out string? isoT)) + { + language = isoT; + } + + dto.Language = language; + dto.ChannelLayout = entity.ChannelLayout; dto.Profile = entity.Profile; dto.AspectRatio = entity.AspectRatio; diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 95b5b43f87..5c8f37fcdb 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -2,6 +2,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -272,7 +273,7 @@ namespace MediaBrowser.Model.Entities // Do not display the language code in display titles if unset or set to a special code. Show it in all other cases (possibly expanded). if (!string.IsNullOrEmpty(Language) && !_specialCodes.Contains(Language, StringComparison.OrdinalIgnoreCase)) { - // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes. + // Get full language string i.e. eng -> English. string fullLanguage = CultureInfo .GetCultures(CultureTypes.NeutralCultures) .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase)) @@ -375,7 +376,7 @@ namespace MediaBrowser.Model.Entities if (!string.IsNullOrEmpty(Language)) { - // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes. + // Get full language string i.e. eng -> English. string fullLanguage = CultureInfo .GetCultures(CultureTypes.NeutralCultures) .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase)) diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs index d9df95325c..f6e65028e4 100644 --- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs +++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Globalization; @@ -61,4 +62,12 @@ public interface ILocalizationManager /// The language. /// The correct for the given language. CultureDto? FindLanguageInfo(string language); + + /// + /// Returns the language in ISO 639-2/T when the input is ISO 639-2/B. + /// + /// The language in ISO 639-2/B. + /// The language in ISO 639-2/T. + /// Whether the language could be converted. + public bool TryGetISO6392TFromB(string isoB, [NotNullWhen(true)] out string? isoT); } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 026da4992a..a7a1e5e811 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using BitFaster.Caching; using Emby.Server.Implementations.Localization; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Configuration; @@ -51,6 +52,30 @@ namespace Jellyfin.Server.Implementations.Tests.Localization Assert.Contains("ger", germany.ThreeLetterISOLanguageNames); } + [Fact] + public async Task TryGetISO6392TFromB_Success() + { + var localizationManager = Setup(new ServerConfiguration + { + UICulture = "de-DE" + }); + await localizationManager.LoadAll(); + + string? isoT; + + // Translation ger -> deu + Assert.True(localizationManager.TryGetISO6392TFromB("ger", out isoT)); + Assert.Equal("deu", isoT); + + // chi -> zho + Assert.True(localizationManager.TryGetISO6392TFromB("chi", out isoT)); + Assert.Equal("zho", isoT); + + // eng is already ISO 639-2/T + Assert.False(localizationManager.TryGetISO6392TFromB("eng", out isoT)); + Assert.Null(isoT); + } + [Theory] [InlineData("de")] [InlineData("deu")] From 98697e75ca112a2047f3735f0904c401e20e4e40 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Tue, 8 Apr 2025 20:58:55 +0000 Subject: [PATCH 184/508] Fix seeking beyond EOF again (#13871) --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index aed7820a6c..2ba6ceea62 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2872,10 +2872,10 @@ namespace MediaBrowser.Controller.MediaEncoding var seekTick = isHlsRemuxing ? time + 5000000L : time; // Seeking beyond EOF makes no sense in transcoding. Clamp the seekTick value to - // [0, RuntimeTicks - 0.5s], so that the muxer gets packets and avoid error codes. + // [0, RuntimeTicks - 5.0s], so that the muxer gets packets and avoid error codes. if (maxTime > 0) { - seekTick = Math.Clamp(seekTick, 0, Math.Max(maxTime - 5000000L, 0)); + seekTick = Math.Clamp(seekTick, 0, Math.Max(maxTime - 50000000L, 0)); } seekParam += string.Format(CultureInfo.InvariantCulture, "-ss {0}", _mediaEncoder.GetTimeParameter(seekTick)); From 16dc1e226091f28565b1ec9f9c5b21ad71b55bf6 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Tue, 8 Apr 2025 22:59:21 +0200 Subject: [PATCH 185/508] Use Guid for parentPrimaryImageItemId (#13874) --- MediaBrowser.Model/Dto/BaseItemDto.cs | 2 +- MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs | 2 +- src/Jellyfin.LiveTv/LiveTvDtoService.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index b38763fbf7..937409111a 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -551,7 +551,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the parent primary image item identifier. /// /// The parent primary image item identifier. - public string ParentPrimaryImageItemId { get; set; } + public Guid? ParentPrimaryImageItemId { get; set; } /// /// Gets or sets the parent primary image tag. diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs index b26f5f45fe..1e8add9430 100644 --- a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs @@ -83,7 +83,7 @@ namespace MediaBrowser.Model.LiveTv /// Gets or sets the parent primary image item identifier. /// /// The parent primary image item identifier. - public string ParentPrimaryImageItemId { get; set; } + public Guid? ParentPrimaryImageItemId { get; set; } /// /// Gets or sets the parent primary image tag. diff --git a/src/Jellyfin.LiveTv/LiveTvDtoService.cs b/src/Jellyfin.LiveTv/LiveTvDtoService.cs index 55b056d3d8..acf168cf11 100644 --- a/src/Jellyfin.LiveTv/LiveTvDtoService.cs +++ b/src/Jellyfin.LiveTv/LiveTvDtoService.cs @@ -221,7 +221,7 @@ namespace Jellyfin.LiveTv try { dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image); - dto.ParentPrimaryImageItemId = program.Id.ToString("N", CultureInfo.InvariantCulture); + dto.ParentPrimaryImageItemId = program.Id; } catch (Exception ex) { @@ -327,7 +327,7 @@ namespace Jellyfin.LiveTv try { dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image); - dto.ParentPrimaryImageItemId = program.Id.ToString("N", CultureInfo.InvariantCulture); + dto.ParentPrimaryImageItemId = program.Id; } catch (Exception ex) { From 070abcd8ff42cfbf423e27a75bbefeaca421460c Mon Sep 17 00:00:00 2001 From: JPVenson Date: Wed, 9 Apr 2025 03:19:01 +0200 Subject: [PATCH 186/508] Fix InheritedParentalRatingSubValue not set (#13880) --- Jellyfin.Server.Implementations/Item/BaseItemRepository.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index f3e7bb3c58..75334f427b 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -620,6 +620,7 @@ public sealed class BaseItemRepository dto.PreferredMetadataCountryCode = entity.PreferredMetadataCountryCode; dto.IsInMixedFolder = entity.IsInMixedFolder; dto.InheritedParentalRatingValue = entity.InheritedParentalRatingValue; + dto.InheritedParentalRatingSubValue = entity.InheritedParentalRatingSubValue; dto.CriticRating = entity.CriticRating; dto.PresentationUniqueKey = entity.PresentationUniqueKey; dto.OriginalTitle = entity.OriginalTitle; From cb59a017a583b4a21f6e6d88acec9877be9386ca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 19:19:21 -0600 Subject: [PATCH 187/508] Update Microsoft to 9.0.4 (#13878) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 50 ++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c81982aa8b..60657fe01f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,29 +25,29 @@ - - + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -76,9 +76,9 @@ - - - + + + From 0bbc6bb31d5a28851a12f8020aac5a258df87a23 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 19:21:28 -0600 Subject: [PATCH 188/508] Update dependency dotnet-ef to 9.0.4 (#13879) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index bc2098a53b..76e6bce7b3 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "9.0.3", + "version": "9.0.4", "commands": [ "dotnet-ef" ] From c152f610ce4acb36d940c032bdf624c269d37d6b Mon Sep 17 00:00:00 2001 From: Markus Prettner Date: Wed, 9 Apr 2025 03:21:57 +0200 Subject: [PATCH 189/508] Fix negated IP addresses without subnet mask not being parsed correctly (#13854) --- MediaBrowser.Common/Net/NetworkUtils.cs | 21 ++++++++++++++----- .../NetworkParseTests.cs | 5 ++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index a498d6271b..24ed47a81b 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -198,14 +198,25 @@ public static partial class NetworkUtils /// True if parsing was successful. public static bool TryParseToSubnet(ReadOnlySpan value, [NotNullWhen(true)] out IPNetwork? result, bool negated = false) { + // If multiple IP addresses are in a comma-separated string, the individual addresses may contain leading and/or trailing whitespace value = value.Trim(); + + bool isAddressNegated = false; + if (value.StartsWith('!')) + { + isAddressNegated = true; + value = value[1..]; // Remove leading '!' character + } + + if (isAddressNegated != negated) + { + result = null; + return false; + } + if (value.Contains('/')) { - if (negated && value.StartsWith("!") && IPNetwork.TryParse(value[1..], out result)) - { - return true; - } - else if (!negated && IPNetwork.TryParse(value, out result)) + if (IPNetwork.TryParse(value, out result)) { return true; } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 4144300da0..ef87e46a78 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -79,7 +79,10 @@ namespace Jellyfin.Networking.Tests [InlineData("[fe80::7add:12ff:febb:c67b%16]")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] public static void TryParseValidIPStringsTrue(string address) - => Assert.True(NetworkUtils.TryParseToSubnet(address, out _)); + { + Assert.True(NetworkUtils.TryParseToSubnet(address, out _)); + Assert.True(NetworkUtils.TryParseToSubnet('!' + address, out _, true)); + } /// /// Checks invalid IP address formats. From 32fe92d8f544d6dee1f0dfb5cccada649524084a Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 9 Apr 2025 09:22:30 +0800 Subject: [PATCH 190/508] Only reselect audio streams when user preference is respected (#13832) --- .../Library/MediaSourceManager.cs | 10 ++++++ Jellyfin.Api/Helpers/MediaInfoHelper.cs | 7 ++++ MediaBrowser.Model/Dlna/StreamBuilder.cs | 32 ++++++++++++++++--- MediaBrowser.Model/Dto/MediaSourceInfo.cs | 4 +++ .../MediaInfo/AudioIndexSource.cs | 30 +++++++++++++++++ 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 MediaBrowser.Model/MediaInfo/AudioIndexSource.cs diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index afe5b14e92..c6cfd5391a 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -427,6 +427,7 @@ namespace Emby.Server.Implementations.Library if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index)) { source.DefaultAudioStreamIndex = index; + source.DefaultAudioIndexSource = AudioIndexSource.User; return; } } @@ -434,6 +435,15 @@ namespace Emby.Server.Implementations.Library var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference); source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack); + if (user.PlayDefaultAudioTrack) + { + source.DefaultAudioIndexSource |= AudioIndexSource.Default; + } + + if (preferredAudio.Count > 0) + { + source.DefaultAudioIndexSource |= AudioIndexSource.Language; + } } public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user) diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 63c9c173b9..454d3f08e3 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -129,6 +129,13 @@ public class MediaInfoHelper var mediaSourcesClone = JsonSerializer.Deserialize(JsonSerializer.SerializeToUtf8Bytes(mediaSources)); if (mediaSourcesClone is not null) { + // Carry over the default audio index source. + // This field is not intended to be exposed to API clients, but it is used internally by the server + for (int i = 0; i < mediaSourcesClone.Length && i < mediaSources.Length; i++) + { + mediaSourcesClone[i].DefaultAudioIndexSource = mediaSources[i].DefaultAudioIndexSource; + } + result.MediaSources = mediaSourcesClone; } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 806900e9ad..61e04a8134 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -665,15 +665,39 @@ namespace MediaBrowser.Model.Dlna // Collect candidate audio streams ICollection candidateAudioStreams = audioStream is null ? [] : [audioStream]; - if (!options.AudioStreamIndex.HasValue || options.AudioStreamIndex < 0) + // When the index is explicitly required by client or the default is specified by user, don't do any stream reselection. + if (!item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.User) && (options.AudioStreamIndex is null or < 0)) { - if (audioStream?.IsDefault == true) + // When user has no preferences allow stream selection on all streams. + if (item.DefaultAudioIndexSource == AudioIndexSource.None && audioStream is not null) { - candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault).ToArray(); + candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio).ToArray(); + if (audioStream.IsDefault) + { + // If default is picked, only allow selection within default streams. + candidateAudioStreams = candidateAudioStreams.Where(stream => stream.IsDefault).ToArray(); + } } - else + + if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Language)) { + // If user has language preference, only allow stream selection within the same language. candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.Language == audioStream?.Language).ToArray(); + if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Default)) + { + var defaultStreamsInPreferredLanguage = candidateAudioStreams.Where(stream => stream.IsDefault).ToArray(); + + // If the user also prefers default streams, try limit selection within default tracks in the same language. + // If there is no default stream in the preferred language, allow selection on all default streams to match the "Play default audio track regardless of language" setting. + candidateAudioStreams = defaultStreamsInPreferredLanguage.Length > 0 + ? defaultStreamsInPreferredLanguage + : item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault).ToArray(); + } + } + else if (item.DefaultAudioIndexSource.HasFlag(AudioIndexSource.Default)) + { + // If user prefers default streams, only allow stream selection on default streams. + candidateAudioStreams = item.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio && stream.IsDefault).ToArray(); } } diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 66de18cfe1..75ccdcf276 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -24,6 +24,7 @@ namespace MediaBrowser.Model.Dto SupportsDirectPlay = true; SupportsProbing = true; UseMostCompatibleTranscodingProfile = false; + DefaultAudioIndexSource = AudioIndexSource.None; } public MediaProtocol Protocol { get; set; } @@ -118,6 +119,9 @@ namespace MediaBrowser.Model.Dto [JsonIgnore] public TranscodeReason TranscodeReasons { get; set; } + [JsonIgnore] + public AudioIndexSource DefaultAudioIndexSource { get; set; } + public int? DefaultAudioStreamIndex { get; set; } public int? DefaultSubtitleStreamIndex { get; set; } diff --git a/MediaBrowser.Model/MediaInfo/AudioIndexSource.cs b/MediaBrowser.Model/MediaInfo/AudioIndexSource.cs new file mode 100644 index 0000000000..810087b926 --- /dev/null +++ b/MediaBrowser.Model/MediaInfo/AudioIndexSource.cs @@ -0,0 +1,30 @@ +using System; + +namespace MediaBrowser.Model.MediaInfo; + +/// +/// How is the audio index determined. +/// +[Flags] +public enum AudioIndexSource +{ + /// + /// The default index when no preference is specified. + /// + None = 0, + + /// + /// The index is calculated whether the track is marked as default or not. + /// + Default = 1 << 0, + + /// + /// The index is calculated whether the track is in preferred language or not. + /// + Language = 1 << 1, + + /// + /// The index is specified by the user. + /// + User = 1 << 2 +} From 294b2f90d10ad8cb6c2cbb16727a70b60e5a4ee2 Mon Sep 17 00:00:00 2001 From: stelle Date: Tue, 8 Apr 2025 12:03:40 -0400 Subject: [PATCH 191/508] Translated using Weblate (Malay) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ms/ --- .../Localization/Core/ms.json | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index c64bcda04d..a3fc7881e3 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -9,14 +9,14 @@ "Channels": "Saluran", "ChapterNameValue": "Bab {0}", "Collections": "Koleksi", - "DeviceOfflineWithName": "{0} telah diputuskan sambungan", + "DeviceOfflineWithName": "{0} telah dinyahsambung", "DeviceOnlineWithName": "{0} telah disambung", - "FailedLoginAttemptWithUserName": "Percubaan log masuk daripada {0} gagal", + "FailedLoginAttemptWithUserName": "Percubaan gagal log masuk daripada {0}", "Favorites": "Kegemaran", - "Folders": "Fail-fail", + "Folders": "Folder-folder", "Genres": "Genre-genre", - "HeaderAlbumArtists": "Album Artis-artis", - "HeaderContinueWatching": "Terus Menonton", + "HeaderAlbumArtists": "Album artis-artis", + "HeaderContinueWatching": "Teruskan Menonton", "HeaderFavoriteAlbums": "Album-album Kegemaran", "HeaderFavoriteArtists": "Artis-artis Kegemaran", "HeaderFavoriteEpisodes": "Episod-episod Kegemaran", @@ -25,26 +25,26 @@ "HeaderLiveTV": "TV Siaran Langsung", "HeaderNextUp": "Seterusnya", "HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman", - "HomeVideos": "Video Personal", - "Inherit": "Mewarisi", - "ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka", + "HomeVideos": "Video Peribadi", + "Inherit": "Warisi", + "ItemAddedWithName": "{0} telah ditambah ke dalam pustaka", "ItemRemovedWithName": "{0} telah dibuang daripada pustaka", "LabelIpAddressValue": "Alamat IP: {0}", "LabelRunningTimeValue": "Masa berjalan: {0}", - "Latest": "Terbaru", - "MessageApplicationUpdated": "Jellyfin Server telah dikemas kini", - "MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini", + "Latest": "Terbaharu", + "MessageApplicationUpdated": "Pelayan Jellyfin telah dikemas kini", + "MessageApplicationUpdatedTo": "Pelayan Jellyfin telah dikemas kini ke {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan bahagian {0} telah dikemas kini", "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini", "MixedContent": "Kandungan campuran", "Movies": "Filem-filem", "Music": "Muzik", "MusicVideos": "Video Muzik", "NameInstallFailed": "{0} pemasangan gagal", - "NameSeasonNumber": "Musim {0}", + "NameSeasonNumber": "Musim ke-{0}", "NameSeasonUnknown": "Musim Tidak Diketahui", - "NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.", - "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia", + "NewVersionIsAvailable": "Versi terbaharu Pelayan Jellyfin telah tersedia untuk dimuat turun.", + "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah tersedia", "NotificationOptionApplicationUpdateInstalled": "Kemas kini aplikasi telah dipasang", "NotificationOptionAudioPlayback": "Ulangmain audio bermula", "NotificationOptionAudioPlaybackStopped": "Ulangmain audio dihentikan", @@ -98,8 +98,8 @@ "TasksLibraryCategory": "Perpustakaan", "TasksMaintenanceCategory": "Penyelenggaraan", "Undefined": "Tidak ditentukan", - "Forced": "Paksa", - "Default": "Asal", + "Forced": "Dipaksa", + "Default": "Lalai", "TaskCleanCache": "Bersihkan Direktori Cache", "TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi.", "TaskRefreshPeople": "Segarkan Orang", From cad8de97018d88ea59c4a5900d0bff9bba73212c Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Thu, 10 Apr 2025 02:40:16 +0200 Subject: [PATCH 192/508] Add Genre cleanup and fix cleanup filter queries (#13891) --- .../Library/Validators/GenresValidator.cs | 23 +++++++++++++++++++ .../Item/BaseItemRepository.cs | 10 ++++++-- .../Entities/InternalItemsQuery.cs | 2 ++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs index e59c62e239..d71e91174d 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs @@ -1,6 +1,9 @@ using System; +using System.Globalization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; @@ -75,6 +78,26 @@ namespace Emby.Server.Implementations.Library.Validators progress.Report(percent); } + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.Genre], + IsDeadGenre = true, + IsLocked = false + }); + + foreach (var item in deadEntities) + { + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); + } + progress.Report(100); } } diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 75334f427b..a569aa09f5 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1962,13 +1962,19 @@ public sealed class BaseItemRepository if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value) { baseQuery = baseQuery - .Where(e => e.ItemValues!.Count(f => f.ItemValue.Type == ItemValueType.Artist || f.ItemValue.Type == ItemValueType.AlbumArtist) == 1); + .Where(e => !context.ItemValues.Where(f => _getAllArtistsValueTypes.Contains(f.Type)).Any(f => f.Value == e.Name)); } if (filter.IsDeadStudio.HasValue && filter.IsDeadStudio.Value) { baseQuery = baseQuery - .Where(e => e.ItemValues!.Count(f => f.ItemValue.Type == ItemValueType.Studios) == 1); + .Where(e => !context.ItemValues.Where(f => _getStudiosValueTypes.Contains(f.Type)).Any(f => f.Value == e.Name)); + } + + if (filter.IsDeadGenre.HasValue && filter.IsDeadGenre.Value) + { + baseQuery = baseQuery + .Where(e => !context.ItemValues.Where(f => _getGenreValueTypes.Contains(f.Type)).Any(f => f.Value == e.Name)); } if (filter.IsDeadPerson.HasValue && filter.IsDeadPerson.Value) diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 9a83dba458..d50f3d075c 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -306,6 +306,8 @@ namespace MediaBrowser.Controller.Entities public bool? IsDeadStudio { get; set; } + public bool? IsDeadGenre { get; set; } + public bool? IsDeadPerson { get; set; } /// From 9a806cf3a4cec5e574bc8aa026738a2847709e37 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 00:29:23 +0000 Subject: [PATCH 193/508] Update dependency FsCheck.Xunit to 3.2.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 60657fe01f..0c08595613 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,7 +17,7 @@ - + From 9ec8790faac74e663e696971d79235915bf387b3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:10:17 +0000 Subject: [PATCH 194/508] Update dependency Svg.Skia to 2.0.0.7 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 60657fe01f..6559a3ba6b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -71,7 +71,7 @@ - + From c9720475662fc47f7059d62f3ccc43f58e12c817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20St=C4=99pie=C5=84?= Date: Fri, 11 Apr 2025 18:25:53 +0200 Subject: [PATCH 195/508] Add polish age ratings (#13851) --- .../Localization/Ratings/pl.json | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 Emby.Server.Implementations/Localization/Ratings/pl.json diff --git a/Emby.Server.Implementations/Localization/Ratings/pl.json b/Emby.Server.Implementations/Localization/Ratings/pl.json new file mode 100644 index 0000000000..c3001ffb37 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Ratings/pl.json @@ -0,0 +1,41 @@ +{ + "countryCode": "pl", + "supportsSubScores": false, + "ratings": [ + { + "ratingStrings": ["b.o.", "AL"], + "ratingScore": { + "score": 0, + "subScore": null + } + }, + { + "ratingStrings": ["7", "od 7 lat"], + "ratingScore": { + "score": 7, + "subScore": null + } + }, + { + "ratingStrings": ["12", "od 12 lat"], + "ratingScore": { + "score": 12, + "subScore": null + } + }, + { + "ratingStrings": ["16", "od 16 lat"], + "ratingScore": { + "score": 16, + "subScore": null + } + }, + { + "ratingStrings": ["18", "od 18 lat", "R"], + "ratingScore": { + "score": 18, + "subScore": null + } + } + ] +} From 874f6895a216fdd9274c1e8a720a8b843274d75c Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 11 Apr 2025 13:58:34 -0400 Subject: [PATCH 196/508] Add ServerName to startup configuration --- Jellyfin.Api/Controllers/StartupController.cs | 2 ++ Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs | 5 +++++ .../Controllers/StartupControllerTests.cs | 4 +++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index a6bc84311f..2278468d9f 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -58,6 +58,7 @@ public class StartupController : BaseJellyfinApiController { return new StartupConfigurationDto { + ServerName = _config.Configuration.ServerName, UICulture = _config.Configuration.UICulture, MetadataCountryCode = _config.Configuration.MetadataCountryCode, PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage @@ -74,6 +75,7 @@ public class StartupController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration) { + _config.Configuration.ServerName = startupConfiguration.ServerName ?? string.Empty; _config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty; _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty; _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty; diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs index 4027078190..1ba23339d0 100644 --- a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs +++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs @@ -5,6 +5,11 @@ namespace Jellyfin.Api.Models.StartupDtos; /// public class StartupConfigurationDto { + /// + /// Gets or sets the server name. + /// + public string? ServerName { get; set; } + /// /// Gets or sets UI language culture. /// diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs index 36861294b5..36ba6c7e35 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs @@ -31,6 +31,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var config = new StartupConfigurationDto() { + ServerName = "NewServer", UICulture = "NewCulture", MetadataCountryCode = "be", PreferredMetadataLanguage = "nl" @@ -44,7 +45,8 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType); var newConfig = await getResponse.Content.ReadFromJsonAsync(_jsonOptions); - Assert.Equal(config.UICulture, newConfig!.UICulture); + Assert.Equal(config.ServerName, newConfig!.ServerName); + Assert.Equal(config.UICulture, newConfig.UICulture); Assert.Equal(config.MetadataCountryCode, newConfig.MetadataCountryCode); Assert.Equal(config.PreferredMetadataLanguage, newConfig.PreferredMetadataLanguage); } From d6fbdcc0f8f11828c865d1fcbd8220c133a791ba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 12 Apr 2025 09:14:07 +0000 Subject: [PATCH 197/508] Update dependency AsyncKeyedLock to 7.1.6 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e2750ee049..766aaaad07 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ - + From afdde7b2439e7f96da0abb9a0989a62770dfb63b Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Sat, 12 Apr 2025 09:12:33 -0400 Subject: [PATCH 198/508] Remove the hashed password from startup users response (#13904) --- Jellyfin.Api/Controllers/StartupController.cs | 3 +-- .../Controllers/StartupControllerTests.cs | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 2278468d9f..09f20558fe 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -114,8 +114,7 @@ public class StartupController : BaseJellyfinApiController var user = _userManager.Users.First(); return new StartupUserDto { - Name = user.Username, - Password = user.Password + Name = user.Username }; } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs index 36ba6c7e35..c8ae2a88af 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs @@ -90,9 +90,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var newUser = await getResponse.Content.ReadFromJsonAsync(_jsonOptions); Assert.NotNull(newUser); Assert.Equal(user.Name, newUser.Name); - Assert.NotNull(newUser.Password); - Assert.NotEmpty(newUser.Password); - Assert.NotEqual(user.Password, newUser.Password); + Assert.Null(newUser.Password); } [Fact] From 61cb53999e0d2bbb25693f2be2b88ec84818e7ab Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sun, 13 Apr 2025 15:43:06 +0200 Subject: [PATCH 199/508] Safeguard against null value trimming in tag results (#13908) --- MediaBrowser.Providers/MediaInfo/AudioFileProber.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 916e2625b0..0bb21b2878 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -180,8 +180,8 @@ namespace MediaBrowser.Providers.MediaInfo // That setter is meant for its own tag parser and external editor usage and will have unwanted side effects // For example, setting the Year property will also set the Date property, which is not what we want here. // To properly handle fallback values, we make a clone of those fields when valid. - var trackTitle = (string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title).Trim(); - var trackAlbum = (string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album).Trim(); + var trackTitle = (string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title)?.Trim(); + var trackAlbum = (string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album)?.Trim(); var trackYear = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year; var trackTrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber; var trackDiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber; From 22c816de0a51f4c437821e4edb5b148cdac9a0a0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 13 Apr 2025 07:43:44 -0600 Subject: [PATCH 200/508] Update dependency Svg.Skia to 2.0.0.8 (#13907) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 766aaaad07..683e1830dc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -71,7 +71,7 @@ - + From 8be8ea60f1e217310b33375cd1437a485cc52583 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Sun, 13 Apr 2025 13:43:58 +0000 Subject: [PATCH 201/508] Add DoVi Profile 5 support for Rockchip RKMPP (#13911) --- .../MediaEncoding/EncodingHelper.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 2ba6ceea62..7c3138002f 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -84,6 +84,7 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly Version _minFFmpegQsvVppOutRangeOption = new Version(7, 0, 1); private readonly Version _minFFmpegVaapiDeviceVendorId = new Version(7, 0, 1); private readonly Version _minFFmpegQsvVppScaleModeOption = new Version(6, 0); + private readonly Version _minFFmpegRkmppHevcDecDoviRpu = new Version(7, 1, 1); private static readonly Regex _containerValidationRegex = new(ContainerValidationRegex, RegexOptions.Compiled); @@ -349,8 +350,17 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.VideoStream.VideoRange == VideoRange.HDR && state.VideoStream.VideoRangeType == VideoRangeType.DOVI) { - // Only native SW decoder and HW accelerator can parse dovi rpu. + // Only native SW decoder, HW accelerator and hevc_rkmpp decoder can parse dovi rpu. var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty; + + var isRkmppDecoder = vidDecoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase); + if (isRkmppDecoder + && _mediaEncoder.EncoderVersion >= _minFFmpegRkmppHevcDecDoviRpu + && string.Equals(state.VideoStream?.Codec, "hevc", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isNvdecDecoder = vidDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase); var isVaapiDecoder = vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); From 6e9e2f500f18d879a9aec8e0e1b57e858bc320e4 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Tue, 15 Apr 2025 04:43:38 +0200 Subject: [PATCH 202/508] Fix Genre cleanup (#13916) --- .../Library/Validators/GenresValidator.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 24 ++++++++----------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs index d71e91174d..364770fcdc 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Validators var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = [BaseItemKind.Genre], + IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre], IsDeadGenre = true, IsLocked = false }); diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index d484266726..d1a6b35841 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -52,7 +52,7 @@ namespace MediaBrowser.Controller.Entities /// The supported image extensions. /// public static readonly string[] SupportedImageExtensions - = new[] { ".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg" }; + = [".png", ".jpg", ".jpeg", ".webp", ".tbn", ".gif", ".svg"]; private static readonly List _supportedExtensions = new List(SupportedImageExtensions) { @@ -448,7 +448,7 @@ namespace MediaBrowser.Controller.Entities return Array.Empty(); } - return new[] { Path }; + return [Path]; } } @@ -1984,7 +1984,7 @@ namespace MediaBrowser.Controller.Entities public void RemoveImage(ItemImageInfo image) { - RemoveImages(new[] { image }); + RemoveImages([image]); } public void RemoveImages(IEnumerable deletedImages) @@ -2019,7 +2019,7 @@ namespace MediaBrowser.Controller.Entities continue; } - (deletedImages ??= new List()).Add(imageInfo); + (deletedImages ??= []).Add(imageInfo); } var anyImagesRemoved = deletedImages?.Count > 0; @@ -2222,11 +2222,7 @@ namespace MediaBrowser.Controller.Entities { return new[] { - new FileSystemMetadata - { - FullName = Path, - IsDirectory = IsFolder - } + FileSystem.GetFileSystemInfo(Path) }.Concat(GetLocalMetadataFilesToDelete()); } @@ -2234,7 +2230,7 @@ namespace MediaBrowser.Controller.Entities { if (IsFolder || !IsInMixedFolder) { - return new List(); + return []; } var filename = System.IO.Path.GetFileNameWithoutExtension(Path); @@ -2490,10 +2486,10 @@ namespace MediaBrowser.Controller.Entities protected virtual List GetEtagValues(User user) { - return new List - { + return + [ DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture) - }; + ]; } public virtual IEnumerable GetAncestorIds() @@ -2513,7 +2509,7 @@ namespace MediaBrowser.Controller.Entities public virtual IEnumerable GetIdsForAncestorQuery() { - return new[] { Id }; + return [Id]; } public virtual double? GetRefreshProgress() From ccc49b109f68c1c3627c6ef35e1d15d48a1e3c9a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:48:45 +0000 Subject: [PATCH 203/508] Update dependency libse to 4.0.12 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 683e1830dc..02e8c146dc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,7 +22,7 @@ - + From e84826297d53745c6aad2c39b9d3b096384ba07c Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Sat, 19 Apr 2025 16:41:30 +0000 Subject: [PATCH 204/508] Fix thumbnail extraction of mpegts videos in FFmpeg 7.1+ (#13942) --- .../Encoder/MediaEncoder.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 9a759ba418..39c0bfed40 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -720,13 +720,11 @@ namespace MediaBrowser.MediaEncoding.Encoder filters.Add(scaler); - // Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case. - // mpegts need larger batch size otherwise the corrupted thumbnail will be created. Larger batch size will lower the processing speed. + // Use ffmpeg to sample N frames and pick the best thumbnail. Have a fall back just in case. var enableThumbnail = !useTradeoff && useIFrame && !string.Equals("wtv", container, StringComparison.OrdinalIgnoreCase); if (enableThumbnail) { - var useLargerBatchSize = string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase); - filters.Add("thumbnail=n=" + (useLargerBatchSize ? "50" : "24")); + filters.Add("thumbnail=n=24"); } // Use SW tonemap on HDR video stream only when the zscale or tonemapx filter is available. @@ -750,14 +748,26 @@ namespace MediaBrowser.MediaEncoding.Encoder var vf = string.Join(',', filters); var mapArg = imageStreamIndex.HasValue ? (" -map 0:" + imageStreamIndex.Value.ToString(CultureInfo.InvariantCulture)) : string.Empty; - var args = string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 -vf {2}{5} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, _threads, isAudio ? string.Empty : GetImageResolutionParameter()); + var args = string.Format( + CultureInfo.InvariantCulture, + "-i {0}{1} -threads {2} -v quiet -vframes 1 -vf {3}{4}{5} -f image2 \"{6}\"", + inputPath, + mapArg, + _threads, + vf, + isAudio ? string.Empty : GetImageResolutionParameter(), + EncodingHelper.GetVideoSyncOption("0", EncoderVersion).Trim(), // passthrough timestamp + tempExtractPath); if (offset.HasValue) { args = string.Format(CultureInfo.InvariantCulture, "-ss {0} ", GetTimeParameter(offset.Value)) + args; } - if (useIFrame && useTradeoff) + // The mpegts demuxer cannot seek to keyframes, so we have to let the + // decoder discard non-keyframes, which may contain corrupted images. + var seekMpegTs = offset.HasValue && string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase); + if ((useIFrame && useTradeoff) || seekMpegTs) { args = "-skip_frame nokey " + args; } From 1c190f79522268fb6b3c870749e2ab9a4b8b22fb Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sat, 19 Apr 2025 12:45:19 -0400 Subject: [PATCH 205/508] Improve video resolution filtering and classification logic (#13332) --- .../Item/BaseItemRepository.cs | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index a569aa09f5..7f4364cf61 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -1309,34 +1309,39 @@ public sealed class BaseItemRepository JellyfinDbContext context, InternalItemsQuery filter) { + const int HDWidth = 1200; + const int UHDWidth = 3800; + const int UHDHeight = 2100; + var minWidth = filter.MinWidth; var maxWidth = filter.MaxWidth; var now = DateTime.UtcNow; - if (filter.IsHD.HasValue) + if (filter.IsHD.HasValue || filter.Is4K.HasValue) { - const int Threshold = 1200; - if (filter.IsHD.Value) - { - minWidth = Threshold; - } - else - { - maxWidth = Threshold - 1; - } - } + bool includeSD = false; + bool includeHD = false; + bool include4K = false; - if (filter.Is4K.HasValue) - { - const int Threshold = 3800; - if (filter.Is4K.Value) + if (filter.IsHD.HasValue && !filter.IsHD.Value) { - minWidth = Threshold; + includeSD = true; } - else + + if (filter.IsHD.HasValue && filter.IsHD.Value) { - maxWidth = Threshold - 1; + includeHD = true; } + + if (filter.Is4K.HasValue && filter.Is4K.Value) + { + include4K = true; + } + + baseQuery = baseQuery.Where(e => + (includeSD && e.Width < HDWidth) || + (includeHD && e.Width >= HDWidth && !(e.Width >= UHDWidth || e.Height >= UHDHeight)) || + (include4K && (e.Width >= UHDWidth || e.Height >= UHDHeight))); } if (minWidth.HasValue) From 269508be9f78901b3a3b2bea88392aeef88359e4 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Sat, 19 Apr 2025 21:08:15 +0200 Subject: [PATCH 206/508] Fix SyncPlay WebSocket OpenAPI schemas (#13946) --- Emby.Server.Implementations/SyncPlay/Group.cs | 16 +-- .../SyncPlay/SyncPlayManager.cs | 8 +- .../Filters/AdditionalModelFilter.cs | 105 ++++++++++++------ .../SyncPlayGroupUpdateCommandMessage.cs | 24 ---- ...layGroupUpdateCommandOfGroupInfoMessage.cs | 25 ----- ...pUpdateCommandOfGroupStateUpdateMessage.cs | 25 ----- ...upUpdateCommandOfPlayQueueUpdateMessage.cs | 25 ----- ...ncPlayGroupUpdateCommandOfStringMessage.cs | 25 ----- .../Session/ISessionManager.cs | 2 +- .../GroupStates/AbstractGroupState.cs | 12 +- .../SyncPlay/GroupStates/WaitingGroupState.cs | 16 +-- .../SyncPlay/IGroupStateContext.cs | 11 +- MediaBrowser.Model/SyncPlay/GroupUpdate.cs | 17 ++- MediaBrowser.Model/SyncPlay/GroupUpdateOfT.cs | 31 ------ .../SyncPlay/GroupUpdateType.cs | 10 -- .../SyncPlayGroupDoesNotExistUpdate.cs | 21 ++++ .../SyncPlay/SyncPlayGroupJoinedUpdate.cs | 21 ++++ .../SyncPlay/SyncPlayGroupLeftUpdate.cs | 21 ++++ .../SyncPlayLibraryAccessDeniedUpdate.cs | 21 ++++ .../SyncPlay/SyncPlayNotInGroupUpdate.cs | 21 ++++ .../SyncPlay/SyncPlayPlayQueueUpdate.cs | 21 ++++ .../SyncPlay/SyncPlayStateUpdate.cs | 21 ++++ .../SyncPlay/SyncPlayUserJoinedUpdate.cs | 21 ++++ .../SyncPlay/SyncPlayUserLeftUpdate.cs | 21 ++++ 24 files changed, 299 insertions(+), 242 deletions(-) delete mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs delete mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs delete mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs delete mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs delete mode 100644 MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs delete mode 100644 MediaBrowser.Model/SyncPlay/GroupUpdateOfT.cs create mode 100644 MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs create mode 100644 MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs create mode 100644 MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs create mode 100644 MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs create mode 100644 MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs create mode 100644 MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs create mode 100644 MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs create mode 100644 MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs create mode 100644 MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs index d47e477938..c2e834ad58 100644 --- a/Emby.Server.Implementations/SyncPlay/Group.cs +++ b/Emby.Server.Implementations/SyncPlay/Group.cs @@ -273,7 +273,7 @@ namespace Emby.Server.Implementations.SyncPlay SetState(waitingState); } - var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo()); + var updateSession = new SyncPlayGroupJoinedUpdate(GroupId, GetInfo()); SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); _state.SessionJoined(this, _state.Type, session, cancellationToken); @@ -291,10 +291,10 @@ namespace Emby.Server.Implementations.SyncPlay { AddSession(session); - var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, GetInfo()); + var updateSession = new SyncPlayGroupJoinedUpdate(GroupId, GetInfo()); SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); - var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); + var updateOthers = new SyncPlayUserJoinedUpdate(GroupId, session.UserName); SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); _state.SessionJoined(this, _state.Type, session, cancellationToken); @@ -314,10 +314,10 @@ namespace Emby.Server.Implementations.SyncPlay RemoveSession(session); - var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, GroupId.ToString()); + var updateSession = new SyncPlayGroupLeftUpdate(GroupId, GroupId.ToString()); SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); - var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName); + var updateOthers = new SyncPlayUserLeftUpdate(GroupId, session.UserName); SendGroupUpdate(session, SyncPlayBroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); _logger.LogInformation("Session {SessionId} left group {GroupId}.", session.Id, GroupId.ToString()); @@ -425,12 +425,6 @@ namespace Emby.Server.Implementations.SyncPlay DateTime.UtcNow); } - /// - public GroupUpdate NewSyncPlayGroupUpdate(GroupUpdateType type, T data) - { - return new GroupUpdate(GroupId, type, data); - } - /// public long SanitizePositionTicks(long? positionTicks) { diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index fdfff8f3b8..92b80e1029 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.SyncPlay { _logger.LogWarning("Session {SessionId} tried to join group {GroupId} that does not exist.", session.Id, request.GroupId); - var error = new GroupUpdate(Guid.Empty, GroupUpdateType.GroupDoesNotExist, string.Empty); + var error = new SyncPlayGroupDoesNotExistUpdate(Guid.Empty, string.Empty); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } @@ -171,7 +171,7 @@ namespace Emby.Server.Implementations.SyncPlay { _logger.LogWarning("Session {SessionId} tried to join group {GroupId} but does not have access to some content of the playing queue.", session.Id, group.GroupId.ToString()); - var error = new GroupUpdate(group.GroupId, GroupUpdateType.LibraryAccessDenied, string.Empty); + var error = new SyncPlayLibraryAccessDeniedUpdate(group.GroupId, string.Empty); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } @@ -248,7 +248,7 @@ namespace Emby.Server.Implementations.SyncPlay { _logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id); - var error = new GroupUpdate(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty); + var error = new SyncPlayNotInGroupUpdate(Guid.Empty, string.Empty); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); } } @@ -327,7 +327,7 @@ namespace Emby.Server.Implementations.SyncPlay { _logger.LogWarning("Session {SessionId} does not belong to any group.", session.Id); - var error = new GroupUpdate(Guid.Empty, GroupUpdateType.NotInGroup, string.Empty); + var error = new SyncPlayNotInGroupUpdate(Guid.Empty, string.Empty); _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); } } diff --git a/Jellyfin.Server/Filters/AdditionalModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs index 4cd0fc231e..421eeecda0 100644 --- a/Jellyfin.Server/Filters/AdditionalModelFilter.cs +++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs @@ -92,17 +92,51 @@ namespace Jellyfin.Server.Filters continue; } - // Additional discriminator needed for GroupUpdate models... - if (messageType == SessionMessageType.SyncPlayGroupUpdate && type != typeof(SyncPlayGroupUpdateCommandMessage)) - { - continue; - } - var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); outboundWebSocketSchemas.Add(schema); outboundWebSocketDiscriminators.Add(messageType.ToString()!, schema.Reference.ReferenceV3); } + // Add custom "SyncPlayGroupUpdateMessage" schema because Swashbuckle cannot generate it for us + var syncPlayGroupUpdateMessageSchema = new OpenApiSchema + { + Type = "object", + Description = "Untyped sync play command.", + Properties = new Dictionary + { + { + "Data", new OpenApiSchema + { + AllOf = + [ + new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(GroupUpdate) } } + ], + Description = "Group update data", + Nullable = false, + } + }, + { "MessageId", new OpenApiSchema { Type = "string", Format = "uuid", Description = "Gets or sets the message id." } }, + { + "MessageType", new OpenApiSchema + { + Enum = Enum.GetValues().Select(type => new OpenApiString(type.ToString())).ToList(), + AllOf = + [ + new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(SessionMessageType) } } + ], + Description = "The different kinds of messages that are used in the WebSocket api.", + Default = new OpenApiString(nameof(SessionMessageType.SyncPlayGroupUpdate)), + ReadOnly = true + } + }, + }, + AdditionalPropertiesAllowed = false, + Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "SyncPlayGroupUpdateMessage" } + }; + context.SchemaRepository.AddDefinition("SyncPlayGroupUpdateMessage", syncPlayGroupUpdateMessageSchema); + outboundWebSocketSchemas.Add(syncPlayGroupUpdateMessageSchema); + outboundWebSocketDiscriminators[nameof(SessionMessageType.SyncPlayGroupUpdate)] = syncPlayGroupUpdateMessageSchema.Reference.ReferenceV3; + var outboundWebSocketMessageSchema = new OpenApiSchema { Type = "object", @@ -140,41 +174,46 @@ namespace Jellyfin.Server.Filters }); // Manually generate sync play GroupUpdate messages. - if (!context.SchemaRepository.Schemas.TryGetValue(nameof(GroupUpdate), out var groupUpdateSchema)) + var groupUpdateTypes = typeof(GroupUpdate<>).Assembly.GetTypes() + .Where(t => t.BaseType != null + && t.BaseType.IsGenericType + && t.BaseType.GetGenericTypeDefinition() == typeof(GroupUpdate<>)) + .ToList(); + + var groupUpdateSchemas = new List(); + var groupUpdateDiscriminators = new Dictionary(); + foreach (var type in groupUpdateTypes) { - groupUpdateSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository); + var groupUpdateType = (GroupUpdateType?)type.GetProperty(nameof(GroupUpdate.Type))?.GetCustomAttribute()?.Value; + if (groupUpdateType is null) + { + continue; + } + + var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); + groupUpdateSchemas.Add(schema); + groupUpdateDiscriminators[groupUpdateType.ToString()!] = schema.Reference.ReferenceV3; } - var groupUpdateOfGroupInfoSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository); - var groupUpdateOfGroupStateSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository); - var groupUpdateOfStringSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository); - var groupUpdateOfPlayQueueSchema = context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate), context.SchemaRepository); - - groupUpdateSchema.OneOf = new List + var groupUpdateSchema = new OpenApiSchema { - groupUpdateOfGroupInfoSchema, - groupUpdateOfGroupStateSchema, - groupUpdateOfStringSchema, - groupUpdateOfPlayQueueSchema - }; - - groupUpdateSchema.Discriminator = new OpenApiDiscriminator - { - PropertyName = nameof(GroupUpdate.Type), - Mapping = new Dictionary + Type = "object", + Description = "Represents the list of possible group update types", + Reference = new OpenApiReference { - { GroupUpdateType.UserJoined.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 }, - { GroupUpdateType.UserLeft.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 }, - { GroupUpdateType.GroupJoined.ToString(), groupUpdateOfGroupInfoSchema.Reference.ReferenceV3 }, - { GroupUpdateType.GroupLeft.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 }, - { GroupUpdateType.StateUpdate.ToString(), groupUpdateOfGroupStateSchema.Reference.ReferenceV3 }, - { GroupUpdateType.PlayQueue.ToString(), groupUpdateOfPlayQueueSchema.Reference.ReferenceV3 }, - { GroupUpdateType.NotInGroup.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 }, - { GroupUpdateType.GroupDoesNotExist.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 }, - { GroupUpdateType.LibraryAccessDenied.ToString(), groupUpdateOfStringSchema.Reference.ReferenceV3 } + Id = nameof(GroupUpdate), + Type = ReferenceType.Schema + }, + OneOf = groupUpdateSchemas, + Discriminator = new OpenApiDiscriminator + { + PropertyName = nameof(GroupUpdate.Type), + Mapping = groupUpdateDiscriminators } }; + context.SchemaRepository.Schemas[nameof(GroupUpdate)] = groupUpdateSchema; + context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository); foreach (var configuration in _serverConfigurationManager.GetConfigurationStores()) diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs deleted file mode 100644 index 6a501aa7ea..0000000000 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandMessage.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.ComponentModel; -using MediaBrowser.Model.Session; -using MediaBrowser.Model.SyncPlay; - -namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; - -/// -/// Untyped sync play command. -/// -public class SyncPlayGroupUpdateCommandMessage : OutboundWebSocketMessage -{ - /// - /// Initializes a new instance of the class. - /// - /// The send command. - public SyncPlayGroupUpdateCommandMessage(GroupUpdate data) - : base(data) - { - } - - /// - [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] - public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; -} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs deleted file mode 100644 index 47f706e2a4..0000000000 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupInfoMessage.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel; -using MediaBrowser.Model.Session; -using MediaBrowser.Model.SyncPlay; - -namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; - -/// -/// Sync play group update command with group info. -/// GroupUpdateTypes: GroupJoined. -/// -public class SyncPlayGroupUpdateCommandOfGroupInfoMessage : OutboundWebSocketMessage> -{ - /// - /// Initializes a new instance of the class. - /// - /// The group info. - public SyncPlayGroupUpdateCommandOfGroupInfoMessage(GroupUpdate data) - : base(data) - { - } - - /// - [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] - public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; -} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs deleted file mode 100644 index 11ddb1e250..0000000000 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel; -using MediaBrowser.Model.Session; -using MediaBrowser.Model.SyncPlay; - -namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; - -/// -/// Sync play group update command with group state update. -/// GroupUpdateTypes: StateUpdate. -/// -public class SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage : OutboundWebSocketMessage> -{ - /// - /// Initializes a new instance of the class. - /// - /// The group info. - public SyncPlayGroupUpdateCommandOfGroupStateUpdateMessage(GroupUpdate data) - : base(data) - { - } - - /// - [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] - public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; -} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs deleted file mode 100644 index 7e73399b1b..0000000000 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel; -using MediaBrowser.Model.Session; -using MediaBrowser.Model.SyncPlay; - -namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; - -/// -/// Sync play group update command with play queue update. -/// GroupUpdateTypes: PlayQueue. -/// -public class SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage : OutboundWebSocketMessage> -{ - /// - /// Initializes a new instance of the class. - /// - /// The play queue update. - public SyncPlayGroupUpdateCommandOfPlayQueueUpdateMessage(GroupUpdate data) - : base(data) - { - } - - /// - [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] - public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; -} diff --git a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs b/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs deleted file mode 100644 index 5b5ccd3eda..0000000000 --- a/MediaBrowser.Controller/Net/WebSocketMessages/Outbound/SyncPlayGroupUpdateCommandOfStringMessage.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel; -using MediaBrowser.Model.Session; -using MediaBrowser.Model.SyncPlay; - -namespace MediaBrowser.Controller.Net.WebSocketMessages.Outbound; - -/// -/// Sync play group update command with string. -/// GroupUpdateTypes: GroupDoesNotExist (error), LibraryAccessDenied (error), NotInGroup (error), GroupLeft (groupId), UserJoined (username), UserLeft (username). -/// -public class SyncPlayGroupUpdateCommandOfStringMessage : OutboundWebSocketMessage> -{ - /// - /// Initializes a new instance of the class. - /// - /// The send command. - public SyncPlayGroupUpdateCommandOfStringMessage(GroupUpdate data) - : base(data) - { - } - - /// - [DefaultValue(SessionMessageType.SyncPlayGroupUpdate)] - public override SessionMessageType MessageType => SessionMessageType.SyncPlayGroupUpdate; -} diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 47bcfdb6ef..2b3afa1174 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -161,7 +161,7 @@ namespace MediaBrowser.Controller.Session /// The identifier of the session. /// The group update. /// The cancellation token. - /// Type of group. + /// The group update type. /// Task. Task SendSyncPlayGroupUpdate(string sessionId, GroupUpdate command, CancellationToken cancellationToken); diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs index 51c95a1bb2..31890c40ae 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/AbstractGroupState.cs @@ -80,7 +80,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates } var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RemoveItems); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); if (playingItemRemoved && !context.PlayQueue.IsItemPlaying()) @@ -106,7 +106,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates } var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.MoveItem); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); } @@ -127,7 +127,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates _ => PlayQueueUpdateReason.Queue }; var playQueueUpdate = context.GetPlayQueueUpdate(reason); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); } @@ -184,7 +184,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates { context.SetRepeatMode(request.Mode); var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.RepeatMode); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); } @@ -193,7 +193,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates { context.SetShuffleMode(request.Mode); var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.ShuffleMode); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); } @@ -221,7 +221,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates { // Notify relevant state change event. var stateUpdate = new GroupStateUpdate(Type, reason.Action); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.StateUpdate, stateUpdate); + var update = new SyncPlayStateUpdate(context.GroupId, stateUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); } diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs index dcc06db1ed..132765b719 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs @@ -78,7 +78,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates // Prepare new session. var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken); context.SetBuffering(session, true); @@ -152,7 +152,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates } var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); // Reset status of sessions and await for all Ready events. @@ -177,7 +177,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates if (result) { var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); // Reset status of sessions and await for all Ready events. @@ -215,7 +215,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates context.RestartCurrentItem(); var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NewPlaylist); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); // Reset status of sessions and await for all Ready events. @@ -336,7 +336,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates _logger.LogDebug("Session {SessionId} reported wrong playlist item in group {GroupId}.", session.Id, context.GroupId.ToString()); var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem); - var updateSession = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var updateSession = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, updateSession, cancellationToken); context.SetBuffering(session, true); @@ -410,7 +410,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates _logger.LogDebug("Session {SessionId} reported wrong playlist item in group {GroupId}.", session.Id, context.GroupId.ToString()); var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.SetCurrentItem); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.CurrentSession, update, cancellationToken); context.SetBuffering(session, true); @@ -583,7 +583,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates { // Send playing-queue update. var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.NextItem); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); // Reset status of sessions and await for all Ready events. @@ -629,7 +629,7 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates { // Send playing-queue update. var playQueueUpdate = context.GetPlayQueueUpdate(PlayQueueUpdateReason.PreviousItem); - var update = context.NewSyncPlayGroupUpdate(GroupUpdateType.PlayQueue, playQueueUpdate); + var update = new SyncPlayPlayQueueUpdate(context.GroupId, playQueueUpdate); context.SendGroupUpdate(session, SyncPlayBroadcastType.AllGroup, update, cancellationToken); // Reset status of sessions and await for all Ready events. diff --git a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs index d2de224503..ddf86be71f 100644 --- a/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs +++ b/MediaBrowser.Controller/SyncPlay/IGroupStateContext.cs @@ -66,11 +66,11 @@ namespace MediaBrowser.Controller.SyncPlay /// /// Sends a GroupUpdate message to the interested sessions. /// - /// The type of the data of the message. /// The current session. /// The filtering type. /// The message to send. /// The cancellation token. + /// The group update type. /// The task. Task SendGroupUpdate(SessionInfo from, SyncPlayBroadcastType type, GroupUpdate message, CancellationToken cancellationToken); @@ -91,15 +91,6 @@ namespace MediaBrowser.Controller.SyncPlay /// The command. SendCommand NewSyncPlayCommand(SendCommandType type); - /// - /// Builds a new group update message. - /// - /// The type of the data of the message. - /// The update type. - /// The data to send. - /// The group update. - GroupUpdate NewSyncPlayGroupUpdate(GroupUpdateType type, T data); - /// /// Sanitizes the PositionTicks, considers the current playing item when available. /// diff --git a/MediaBrowser.Model/SyncPlay/GroupUpdate.cs b/MediaBrowser.Model/SyncPlay/GroupUpdate.cs index ec67d7ea87..7944434999 100644 --- a/MediaBrowser.Model/SyncPlay/GroupUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/GroupUpdate.cs @@ -5,15 +5,18 @@ namespace MediaBrowser.Model.SyncPlay; /// /// Group update without data. /// -public abstract class GroupUpdate +/// The type of the update data. +public abstract class GroupUpdate { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The group identifier. - protected GroupUpdate(Guid groupId) + /// The update data. + protected GroupUpdate(Guid groupId, T data) { GroupId = groupId; + Data = data; } /// @@ -22,9 +25,15 @@ public abstract class GroupUpdate /// The group identifier. public Guid GroupId { get; } + /// + /// Gets the update data. + /// + /// The update data. + public T Data { get; } + /// /// Gets the update type. /// /// The update type. - public GroupUpdateType Type { get; init; } + public abstract GroupUpdateType Type { get; } } diff --git a/MediaBrowser.Model/SyncPlay/GroupUpdateOfT.cs b/MediaBrowser.Model/SyncPlay/GroupUpdateOfT.cs deleted file mode 100644 index 25cd444611..0000000000 --- a/MediaBrowser.Model/SyncPlay/GroupUpdateOfT.cs +++ /dev/null @@ -1,31 +0,0 @@ -#pragma warning disable SA1649 - -using System; - -namespace MediaBrowser.Model.SyncPlay; - -/// -/// Class GroupUpdate. -/// -/// The type of the data of the message. -public class GroupUpdate : GroupUpdate -{ - /// - /// Initializes a new instance of the class. - /// - /// The group identifier. - /// The update type. - /// The update data. - public GroupUpdate(Guid groupId, GroupUpdateType type, T data) - : base(groupId) - { - Data = data; - Type = type; - } - - /// - /// Gets the update data. - /// - /// The update data. - public T Data { get; } -} diff --git a/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs b/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs index 907d1defe0..e792229a47 100644 --- a/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs +++ b/MediaBrowser.Model/SyncPlay/GroupUpdateType.cs @@ -45,16 +45,6 @@ namespace MediaBrowser.Model.SyncPlay /// GroupDoesNotExist, - /// - /// The create-group-denied error. Sent when a user tries to create a group without required permissions. - /// - CreateGroupDenied, - - /// - /// The join-group-denied error. Sent when a user tries to join a group without required permissions. - /// - JoinGroupDenied, - /// /// The library-access-denied error. Sent when a user tries to join a group without required access to the library. /// diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs new file mode 100644 index 0000000000..7e2d10c8b8 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel; + +namespace MediaBrowser.Model.SyncPlay; + +/// +public class SyncPlayGroupDoesNotExistUpdate : GroupUpdate +{ + /// + /// Initializes a new instance of the class. + /// + /// The groupId. + /// The data. + public SyncPlayGroupDoesNotExistUpdate(Guid groupId, string data) : base(groupId, data) + { + } + + /// + [DefaultValue(GroupUpdateType.GroupDoesNotExist)] + public override GroupUpdateType Type => GroupUpdateType.GroupDoesNotExist; +} diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs new file mode 100644 index 0000000000..bfb49152a3 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel; + +namespace MediaBrowser.Model.SyncPlay; + +/// +public class SyncPlayGroupJoinedUpdate : GroupUpdate +{ + /// + /// Initializes a new instance of the class. + /// + /// The groupId. + /// The data. + public SyncPlayGroupJoinedUpdate(Guid groupId, GroupInfoDto data) : base(groupId, data) + { + } + + /// + [DefaultValue(GroupUpdateType.GroupJoined)] + public override GroupUpdateType Type => GroupUpdateType.GroupJoined; +} diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs new file mode 100644 index 0000000000..5ff60c5c27 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel; + +namespace MediaBrowser.Model.SyncPlay; + +/// +public class SyncPlayGroupLeftUpdate : GroupUpdate +{ + /// + /// Initializes a new instance of the class. + /// + /// The groupId. + /// The data. + public SyncPlayGroupLeftUpdate(Guid groupId, string data) : base(groupId, data) + { + } + + /// + [DefaultValue(GroupUpdateType.GroupLeft)] + public override GroupUpdateType Type => GroupUpdateType.GroupLeft; +} diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs new file mode 100644 index 0000000000..0d9a722f78 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel; + +namespace MediaBrowser.Model.SyncPlay; + +/// +public class SyncPlayLibraryAccessDeniedUpdate : GroupUpdate +{ + /// + /// Initializes a new instance of the class. + /// + /// The groupId. + /// The data. + public SyncPlayLibraryAccessDeniedUpdate(Guid groupId, string data) : base(groupId, data) + { + } + + /// + [DefaultValue(GroupUpdateType.LibraryAccessDenied)] + public override GroupUpdateType Type => GroupUpdateType.LibraryAccessDenied; +} diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs new file mode 100644 index 0000000000..a3b610f619 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel; + +namespace MediaBrowser.Model.SyncPlay; + +/// +public class SyncPlayNotInGroupUpdate : GroupUpdate +{ + /// + /// Initializes a new instance of the class. + /// + /// The groupId. + /// The data. + public SyncPlayNotInGroupUpdate(Guid groupId, string data) : base(groupId, data) + { + } + + /// + [DefaultValue(GroupUpdateType.NotInGroup)] + public override GroupUpdateType Type => GroupUpdateType.NotInGroup; +} diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs new file mode 100644 index 0000000000..83d9bd40bc --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel; + +namespace MediaBrowser.Model.SyncPlay; + +/// +public class SyncPlayPlayQueueUpdate : GroupUpdate +{ + /// + /// Initializes a new instance of the class. + /// + /// The groupId. + /// The data. + public SyncPlayPlayQueueUpdate(Guid groupId, PlayQueueUpdate data) : base(groupId, data) + { + } + + /// + [DefaultValue(GroupUpdateType.PlayQueue)] + public override GroupUpdateType Type => GroupUpdateType.PlayQueue; +} diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs new file mode 100644 index 0000000000..744ca46a0b --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel; + +namespace MediaBrowser.Model.SyncPlay; + +/// +public class SyncPlayStateUpdate : GroupUpdate +{ + /// + /// Initializes a new instance of the class. + /// + /// The groupId. + /// The data. + public SyncPlayStateUpdate(Guid groupId, GroupStateUpdate data) : base(groupId, data) + { + } + + /// + [DefaultValue(GroupUpdateType.StateUpdate)] + public override GroupUpdateType Type => GroupUpdateType.StateUpdate; +} diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs new file mode 100644 index 0000000000..e8c6b4df41 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel; + +namespace MediaBrowser.Model.SyncPlay; + +/// +public class SyncPlayUserJoinedUpdate : GroupUpdate +{ + /// + /// Initializes a new instance of the class. + /// + /// The groupId. + /// The data. + public SyncPlayUserJoinedUpdate(Guid groupId, string data) : base(groupId, data) + { + } + + /// + [DefaultValue(GroupUpdateType.UserJoined)] + public override GroupUpdateType Type => GroupUpdateType.UserJoined; +} diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs new file mode 100644 index 0000000000..97be8e63a8 --- /dev/null +++ b/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel; + +namespace MediaBrowser.Model.SyncPlay; + +/// +public class SyncPlayUserLeftUpdate : GroupUpdate +{ + /// + /// Initializes a new instance of the class. + /// + /// The groupId. + /// The data. + public SyncPlayUserLeftUpdate(Guid groupId, string data) : base(groupId, data) + { + } + + /// + [DefaultValue(GroupUpdateType.UserLeft)] + public override GroupUpdateType Type => GroupUpdateType.UserLeft; +} From 7df6e0b16f8e6b3026955e31417906b3bcbba290 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 19 Apr 2025 22:08:24 +0300 Subject: [PATCH 207/508] Add port awareness to startup server (#13913) --- .../Extensions/WebHostBuilderExtensions.cs | 144 ++++++--- Jellyfin.Server/Program.cs | 16 +- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 74 ++++- .../Manager/NetworkManager.cs | 287 ++++++++++-------- 4 files changed, 329 insertions(+), 192 deletions(-) diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs index 7695c0d9ee..be9cf0f154 100644 --- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -1,10 +1,14 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net; +using System.Security.Cryptography.X509Certificates; using Jellyfin.Server.Helpers; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -35,56 +39,98 @@ public static class WebHostBuilderExtensions return builder .UseKestrel((builderContext, options) => { - var addresses = appHost.NetManager.GetAllBindInterfaces(false); - - bool flagged = false; - foreach (var netAdd in addresses) - { - var address = netAdd.Address; - logger.LogInformation("Kestrel is listening on {Address}", address.Equals(IPAddress.IPv6Any) ? "all interfaces" : address); - options.Listen(netAdd.Address, appHost.HttpPort); - if (appHost.ListenWithHttps) - { - options.Listen( - address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps(appHost.Certificate)); - } - else if (builderContext.HostingEnvironment.IsDevelopment()) - { - try - { - options.Listen( - address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps()); - } - catch (InvalidOperationException) - { - if (!flagged) - { - logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted"); - flagged = true; - } - } - } - } - - // Bind to unix socket (only on unix systems) - if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix) - { - var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths); - - // Workaround for https://github.com/aspnet/AspNetCore/issues/14134 - if (File.Exists(socketPath)) - { - File.Delete(socketPath); - } - - options.ListenUnixSocket(socketPath); - logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); - } + SetupJellyfinWebServer( + appHost.NetManager.GetAllBindInterfaces(false), + appHost.HttpPort, + appHost.ListenWithHttps ? appHost.HttpsPort : null, + appHost.Certificate, + startupConfig, + appPaths, + logger, + builderContext, + options); }) .UseStartup(context => new Startup(appHost, context.Configuration)); } + + /// + /// Configures a Kestrel type webServer to bind to the specific arguments. + /// + /// The IP addresses that should be listend to. + /// The http port. + /// If set the https port. If set you must also set the certificate. + /// The certificate used for https port. + /// The startup config. + /// The app paths. + /// A logger. + /// The kestrel build pipeline context. + /// The kestrel server options. + /// Will be thrown when a https port is set but no or an invalid certificate is provided. + public static void SetupJellyfinWebServer( + IReadOnlyList addresses, + int httpPort, + int? httpsPort, + X509Certificate2? certificate, + IConfiguration startupConfig, + IApplicationPaths appPaths, + ILogger logger, + WebHostBuilderContext builderContext, + KestrelServerOptions options) + { + bool flagged = false; + foreach (var netAdd in addresses) + { + var address = netAdd.Address; + logger.LogInformation("Kestrel is listening on {Address}", address.Equals(IPAddress.IPv6Any) ? "all interfaces" : address); + options.Listen(netAdd.Address, httpPort); + if (httpsPort.HasValue) + { + if (builderContext.HostingEnvironment.IsDevelopment()) + { + try + { + options.Listen( + address, + httpsPort.Value, + listenOptions => listenOptions.UseHttps()); + } + catch (InvalidOperationException) + { + if (!flagged) + { + logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted"); + flagged = true; + } + } + } + else + { + if (certificate is null) + { + throw new InvalidOperationException("Cannot run jellyfin with https without setting a valid certificate."); + } + + options.Listen( + address, + httpsPort.Value, + listenOptions => listenOptions.UseHttps(certificate)); + } + } + } + + // Bind to unix socket (only on unix systems) + if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix) + { + var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths); + + // Workaround for https://github.com/aspnet/AspNetCore/issues/14134 + if (File.Exists(socketPath)) + { + File.Delete(socketPath); + } + + options.ListenUnixSocket(socketPath); + logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath); + } + } } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index e661d0d4ab..55a4a00878 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -46,7 +46,7 @@ namespace Jellyfin.Server public const string LoggingConfigFileSystem = "logging.json"; private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); - private static SetupServer _setupServer = new(); + private static SetupServer? _setupServer; private static CoreAppHost? _appHost; private static IHost? _jellyfinHost = null; private static long _startTimestamp; @@ -75,7 +75,6 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); - await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -88,7 +87,8 @@ namespace Jellyfin.Server // Create an instance of the application configuration to use for application startup IConfiguration startupConfig = CreateAppConfiguration(options, appPaths); - + _setupServer = new SetupServer(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost, _loggerFactory, startupConfig); + await _setupServer.RunAsync().ConfigureAwait(false); StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths); _logger = _loggerFactory.CreateLogger("Main"); @@ -130,10 +130,12 @@ namespace Jellyfin.Server if (_restartOnShutdown) { _startTimestamp = Stopwatch.GetTimestamp(); - _setupServer = new SetupServer(); - await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); + await _setupServer.StopAsync().ConfigureAwait(false); + await _setupServer.RunAsync().ConfigureAwait(false); } } while (_restartOnShutdown); + + _setupServer.Dispose(); } private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) @@ -170,9 +172,7 @@ namespace Jellyfin.Server try { - await _setupServer.StopAsync().ConfigureAwait(false); - _setupServer.Dispose(); - _setupServer = null!; + await _setupServer!.StopAsync().ConfigureAwait(false); await _jellyfinHost.StartAsync().ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 9e2cf5bc8b..3d4810bd7f 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -4,6 +4,9 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using Emby.Server.Implementations.Configuration; +using Emby.Server.Implementations.Serialization; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -11,9 +14,12 @@ using MediaBrowser.Model.System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; namespace Jellyfin.Server.ServerSetupApp; @@ -22,20 +28,45 @@ namespace Jellyfin.Server.ServerSetupApp; /// public sealed class SetupServer : IDisposable { + private readonly Func _networkManagerFactory; + private readonly IApplicationPaths _applicationPaths; + private readonly Func _serverFactory; + private readonly ILoggerFactory _loggerFactory; + private readonly IConfiguration _startupConfiguration; + private readonly ServerConfigurationManager _configurationManager; private IHost? _startupServer; private bool _disposed; /// - /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup. + /// Initializes a new instance of the class. /// /// The networkmanager. /// The application paths. - /// The servers application host. - /// A Task. - public async Task RunAsync( + /// The servers application host. + /// The logger factory. + /// The startup configuration. + public SetupServer( Func networkManagerFactory, IApplicationPaths applicationPaths, - Func serverApplicationHost) + Func serverApplicationHostFactory, + ILoggerFactory loggerFactory, + IConfiguration startupConfiguration) + { + _networkManagerFactory = networkManagerFactory; + _applicationPaths = applicationPaths; + _serverFactory = serverApplicationHostFactory; + _loggerFactory = loggerFactory; + _startupConfiguration = startupConfiguration; + var xmlSerializer = new MyXmlSerializer(); + _configurationManager = new ServerConfigurationManager(_applicationPaths, loggerFactory, xmlSerializer); + _configurationManager.RegisterConfiguration(); + } + + /// + /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup. + /// + /// A Task. + public async Task RunAsync() { ThrowIfDisposed(); _startupServer = Host.CreateDefaultBuilder() @@ -48,7 +79,23 @@ public sealed class SetupServer : IDisposable .ConfigureWebHostDefaults(webHostBuilder => { webHostBuilder - .UseKestrel() + .UseKestrel((builderContext, options) => + { + var config = _configurationManager.GetNetworkConfiguration()!; + var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogger(), config.EnableIPv4, config.EnableIPv6); + knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.ToList(), config.EnableIPv4, config.EnableIPv6); + var bindInterfaces = NetworkManager.GetAllBindInterfaces(false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6); + Extensions.WebHostBuilderExtensions.SetupJellyfinWebServer( + bindInterfaces, + config.InternalHttpPort, + null, + null, + _startupConfiguration, + _applicationPaths, + _loggerFactory.CreateLogger(), + builderContext, + options); + }) .Configure(app => { app.UseHealthChecks("/health"); @@ -57,14 +104,14 @@ public sealed class SetupServer : IDisposable { loggerRoute.Run(async context => { - var networkManager = networkManagerFactory(); + var networkManager = _networkManagerFactory(); if (context.Connection.RemoteIpAddress is null || networkManager is null || !networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress)) { context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return; } - var logFilePath = new DirectoryInfo(applicationPaths.LogDirectoryPath) + var logFilePath = new DirectoryInfo(_applicationPaths.LogDirectoryPath) .EnumerateFiles() .OrderBy(f => f.CreationTimeUtc) .FirstOrDefault() @@ -80,20 +127,20 @@ public sealed class SetupServer : IDisposable { systemRoute.Run(async context => { - var jfApplicationHost = serverApplicationHost(); + var jfApplicationHost = _serverFactory(); var retryCounter = 0; while (jfApplicationHost is null && retryCounter < 5) { await Task.Delay(500).ConfigureAwait(false); - jfApplicationHost = serverApplicationHost(); + jfApplicationHost = _serverFactory(); retryCounter++; } if (jfApplicationHost is null) { context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; - context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60"); + context.Response.Headers.RetryAfter = new StringValues("5"); return; } @@ -114,9 +161,10 @@ public sealed class SetupServer : IDisposable app.Run((context) => { context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; - context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60"); + context.Response.Headers.RetryAfter = new StringValues("5"); + context.Response.Headers.ContentType = new StringValues("text/html"); context.Response.WriteAsync("

Jellyfin Server still starting. Please wait.

"); - var networkManager = networkManagerFactory(); + var networkManager = _networkManagerFactory(); if (networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress)) { context.Response.WriteAsync("

You can download the current logfiles here.

"); diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 6f6ee51461..80a5741df9 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; +using J2N.Collections.Generic.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; @@ -214,81 +215,92 @@ public class NetworkManager : INetworkManager, IDisposable { lock (_initLock) { - _logger.LogDebug("Refreshing interfaces."); + _interfaces = GetInterfacesCore(_logger, IsIPv4Enabled, IsIPv6Enabled).ToList(); + } + } - var interfaces = new List(); + /// + /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state. + /// + /// The logger. + /// If true evaluates IPV4 type ip addresses. + /// If true evaluates IPV6 type ip addresses. + /// A list of all locally known up addresses and submasks that are to be considered usable. + public static IReadOnlyList GetInterfacesCore(ILogger logger, bool isIPv4Enabled, bool isIPv6Enabled) + { + logger.LogDebug("Refreshing interfaces."); - try + var interfaces = new List(); + + try + { + var nics = NetworkInterface.GetAllNetworkInterfaces() + .Where(i => i.OperationalStatus == OperationalStatus.Up); + + foreach (NetworkInterface adapter in nics) { - var nics = NetworkInterface.GetAllNetworkInterfaces() - .Where(i => i.OperationalStatus == OperationalStatus.Up); - - foreach (NetworkInterface adapter in nics) + try { - try + var ipProperties = adapter.GetIPProperties(); + + // Populate interface list + foreach (var info in ipProperties.UnicastAddresses) { - var ipProperties = adapter.GetIPProperties(); - - // Populate interface list - foreach (var info in ipProperties.UnicastAddresses) + if (isIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) { - if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) { - var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) - { - Index = ipProperties.GetIPv4Properties().Index, - Name = adapter.Name, - SupportsMulticast = adapter.SupportsMulticast - }; + Index = ipProperties.GetIPv4Properties().Index, + Name = adapter.Name, + SupportsMulticast = adapter.SupportsMulticast + }; - interfaces.Add(interfaceObject); - } - else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) + interfaces.Add(interfaceObject); + } + else if (isIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) + { + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) { - var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) - { - Index = ipProperties.GetIPv6Properties().Index, - Name = adapter.Name, - SupportsMulticast = adapter.SupportsMulticast - }; + Index = ipProperties.GetIPv6Properties().Index, + Name = adapter.Name, + SupportsMulticast = adapter.SupportsMulticast + }; - interfaces.Add(interfaceObject); - } + interfaces.Add(interfaceObject); } } - catch (Exception ex) - { - // Ignore error, and attempt to continue. - _logger.LogError(ex, "Error encountered parsing interfaces."); - } } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error obtaining interfaces."); - } - - // If no interfaces are found, fallback to loopback interfaces. - if (interfaces.Count == 0) - { - _logger.LogWarning("No interface information available. Using loopback interface(s)."); - - if (IsIPv4Enabled) + catch (Exception ex) { - interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); - } - - if (IsIPv6Enabled) - { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); + // Ignore error, and attempt to continue. + logger.LogError(ex, "Error encountered parsing interfaces."); } } - - _logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", interfaces.Count); - _logger.LogDebug("Interfaces addresses: {Addresses}", interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString())); - - _interfaces = interfaces; } + catch (Exception ex) + { + logger.LogError(ex, "Error obtaining interfaces."); + } + + // If no interfaces are found, fallback to loopback interfaces. + if (interfaces.Count == 0) + { + logger.LogWarning("No interface information available. Using loopback interface(s)."); + + if (isIPv4Enabled) + { + interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); + } + + if (isIPv6Enabled) + { + interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); + } + } + + logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", interfaces.Count); + logger.LogDebug("Interfaces addresses: {Addresses}", interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString())); + return interfaces; } /// @@ -345,66 +357,78 @@ public class NetworkManager : INetworkManager, IDisposable { lock (_initLock) { - // Respect explicit bind addresses - var interfaces = _interfaces.ToList(); - var localNetworkAddresses = config.LocalNetworkAddresses; - if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0])) - { - var bindAddresses = localNetworkAddresses.Select(p => NetworkUtils.TryParseToSubnet(p, out var network) - ? network.Prefix - : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Address) - .FirstOrDefault() ?? IPAddress.None)) - .Where(x => x != IPAddress.None) - .ToHashSet(); - interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); - - if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback))) - { - interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); - } - - if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback))) - { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); - } - } - - // Remove all interfaces matching any virtual machine interface prefix - if (config.IgnoreVirtualInterfaces) - { - // Remove potentially existing * and split config string into prefixes - var virtualInterfacePrefixes = config.VirtualInterfaceNames - .Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase)); - - // Check all interfaces for matches against the prefixes and remove them - if (_interfaces.Count > 0) - { - foreach (var virtualInterfacePrefix in virtualInterfacePrefixes) - { - interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase)); - } - } - } - - // Remove all IPv4 interfaces if IPv4 is disabled - if (!IsIPv4Enabled) - { - interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork); - } - - // Remove all IPv6 interfaces if IPv6 is disabled - if (!IsIPv6Enabled) - { - interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); - } - - // 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(); + _interfaces = FilterBindSettings(config, _interfaces, IsIPv4Enabled, IsIPv6Enabled).ToList(); } } + /// + /// Filteres a list of bind addresses and exclusions on available interfaces. + /// + /// The network config to be filtered by. + /// A list of possible interfaces to be filtered. + /// If true evaluates IPV4 type ip addresses. + /// If true evaluates IPV6 type ip addresses. + /// A list of all locally known up addresses and submasks that are to be considered usable. + public static IReadOnlyList FilterBindSettings(NetworkConfiguration config, IList interfaces, bool isIPv4Enabled, bool isIPv6Enabled) + { + // Respect explicit bind addresses + var localNetworkAddresses = config.LocalNetworkAddresses; + if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0])) + { + var bindAddresses = localNetworkAddresses.Select(p => NetworkUtils.TryParseToSubnet(p, out var network) + ? network.Prefix + : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Address) + .FirstOrDefault() ?? IPAddress.None)) + .Where(x => x != IPAddress.None) + .ToHashSet(); + interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); + + if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback))) + { + interfaces.Add(new IPData(IPAddress.Loopback, NetworkConstants.IPv4RFC5735Loopback, "lo")); + } + + if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback))) + { + interfaces.Add(new IPData(IPAddress.IPv6Loopback, NetworkConstants.IPv6RFC4291Loopback, "lo")); + } + } + + // Remove all interfaces matching any virtual machine interface prefix + if (config.IgnoreVirtualInterfaces) + { + // Remove potentially existing * and split config string into prefixes + var virtualInterfacePrefixes = config.VirtualInterfaceNames + .Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase)); + + // Check all interfaces for matches against the prefixes and remove them + if (interfaces.Count > 0) + { + foreach (var virtualInterfacePrefix in virtualInterfacePrefixes) + { + interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase)); + } + } + } + + // Remove all IPv4 interfaces if IPv4 is disabled + if (!isIPv4Enabled) + { + interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork); + } + + // Remove all IPv6 interfaces if IPv6 is disabled + if (!isIPv6Enabled) + { + interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); + } + + // 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 + return interfaces.DistinctBy(iface => iface.Address).ToList(); + } + /// /// Initializes the remote address values. /// @@ -720,28 +744,47 @@ public class NetworkManager : INetworkManager, IDisposable /// public IReadOnlyList GetAllBindInterfaces(bool individualInterfaces = false) { - var config = _configurationManager.GetNetworkConfiguration(); + return NetworkManager.GetAllBindInterfaces(individualInterfaces, _configurationManager, _interfaces, IsIPv4Enabled, IsIPv6Enabled); + } + + /// + /// Reads the jellyfin configuration of the configuration manager and produces a list of interfaces that should be bound. + /// + /// Defines that only known interfaces should be used. + /// The ConfigurationManager. + /// The known interfaces that gets returned if possible or instructed. + /// Include IPV4 type interfaces. + /// Include IPV6 type interfaces. + /// A list of ip address of which jellyfin should bind to. + public static IReadOnlyList GetAllBindInterfaces( + bool individualInterfaces, + IConfigurationManager configurationManager, + IReadOnlyList knownInterfaces, + bool readIpv4, + bool readIpv6) + { + var config = configurationManager.GetNetworkConfiguration(); var localNetworkAddresses = config.LocalNetworkAddresses; - if ((localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0]) && _interfaces.Count > 0) || individualInterfaces) + if ((localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0]) && knownInterfaces.Count > 0) || individualInterfaces) { - return _interfaces; + return knownInterfaces; } // No bind address and no exclusions, so listen on all interfaces. var result = new List(); - if (IsIPv4Enabled && IsIPv6Enabled) + if (readIpv4 && readIpv6) { // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default result.Add(new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any)); } - else if (IsIPv4Enabled) + else if (readIpv4) { result.Add(new IPData(IPAddress.Any, NetworkConstants.IPv4Any)); } - else if (IsIPv6Enabled) + else if (readIpv6) { // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses too. - foreach (var iface in _interfaces) + foreach (var iface in knownInterfaces) { if (iface.AddressFamily == AddressFamily.InterNetworkV6) { From 74230131a199e33894cf1778cee3579cd3c75011 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 19 Apr 2025 21:08:29 +0200 Subject: [PATCH 208/508] Fix OverflowException when scanning media with a very short duration (#13949) --- .../Probing/ProbeResultNormalizer.cs | 2 +- .../Probing/ProbeResultNormalizerTests.cs | 34 +++ .../Probing/video_single_frame_mjpeg.json | 209 ++++++++++++++++++ 3 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_single_frame_mjpeg.json diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index a98dbe5970..5784deacd2 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -965,7 +965,7 @@ namespace MediaBrowser.MediaEncoding.Probing // Get average bitrate info from tag "NUMBER_OF_BYTES" and "DURATION" if possible. var durationInSeconds = GetRuntimeSecondsFromTags(streamInfo); var bytes = GetNumberOfBytesFromTags(streamInfo); - if (durationInSeconds is not null && bytes is not null) + if (durationInSeconds is not null && durationInSeconds.Value >= 1 && bytes is not null) { bps = Convert.ToInt32(bytes * 8 / durationInSeconds, CultureInfo.InvariantCulture); if (bps > 0) diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index df51d39cb7..94710a0957 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -288,6 +288,40 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.True(res.VideoStream.IsDefault); } + [Fact] + public void GetMediaInfo_VideoWithSingleFrameMjpeg_Success() + { + var bytes = File.ReadAllBytes("Test Data/Probing/video_single_frame_mjpeg.json"); + + var internalMediaInfoResult = JsonSerializer.Deserialize(bytes, _jsonOptions); + MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_interlaced.mp4", MediaProtocol.File); + + Assert.Equal(3, res.MediaStreams.Count); + + Assert.NotNull(res.VideoStream); + Assert.Equal(res.MediaStreams[0], res.VideoStream); + Assert.Equal(0, res.VideoStream.Index); + Assert.Equal("h264", res.VideoStream.Codec); + Assert.Equal("High", res.VideoStream.Profile); + Assert.Equal(MediaStreamType.Video, res.VideoStream.Type); + Assert.Equal(1080, res.VideoStream.Height); + Assert.Equal(1920, res.VideoStream.Width); + Assert.False(res.VideoStream.IsInterlaced); + Assert.Equal("16:9", res.VideoStream.AspectRatio); + Assert.Equal("yuv420p", res.VideoStream.PixelFormat); + Assert.Equal(42d, res.VideoStream.Level); + Assert.Equal(1, res.VideoStream.RefFrames); + Assert.True(res.VideoStream.IsAVC); + Assert.Equal(50f, res.VideoStream.RealFrameRate); + Assert.Equal("1/1000", res.VideoStream.TimeBase); + Assert.Equal(8, res.VideoStream.BitDepth); + Assert.True(res.VideoStream.IsDefault); + + var mjpeg = res.MediaStreams[2]; + Assert.NotNull(mjpeg); + Assert.Equal("mjpeg", mjpeg.Codec); + } + [Fact] public void GetMediaInfo_MusicVideo_Success() { diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_single_frame_mjpeg.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_single_frame_mjpeg.json new file mode 100644 index 0000000000..261f044bb9 --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_single_frame_mjpeg.json @@ -0,0 +1,209 @@ +{ + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + "profile": "High", + "codec_type": "video", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 1920, + "height": 1080, + "coded_width": 1920, + "coded_height": 1080, + "closed_captions": 0, + "film_grain": 0, + "has_b_frames": 0, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "16:9", + "pix_fmt": "yuv420p", + "level": 42, + "chroma_location": "left", + "field_order": "progressive", + "refs": 1, + "is_avc": "true", + "nal_length_size": "4", + "r_frame_rate": "50/1", + "avg_frame_rate": "50/1", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "bits_per_raw_sample": "8", + "extradata_size": 55, + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "non_diegetic": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "deu", + "HANDLER_NAME": "VideoHandler", + "VENDOR_ID": "[0][0][0][0]", + "BPS": "3950584", + "DURATION": "00:00:10.000000000", + "NUMBER_OF_FRAMES": "500", + "NUMBER_OF_BYTES": "4938231", + "_STATISTICS_WRITING_APP": "mkvpropedit v90.0 ('Hanging On') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2025-04-19 10:37:57", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 1, + "codec_name": "aac", + "codec_long_name": "AAC (Advanced Audio Coding)", + "profile": "LC", + "codec_type": "audio", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 2, + "channel_layout": "stereo", + "bits_per_sample": 0, + "initial_padding": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "extradata_size": 2, + "disposition": { + "default": 1, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "non_diegetic": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "deu", + "HANDLER_NAME": "SoundHandler", + "VENDOR_ID": "[0][0][0][0]", + "BPS": "255785", + "DURATION": "00:00:09.984000000", + "NUMBER_OF_FRAMES": "469", + "NUMBER_OF_BYTES": "319220", + "_STATISTICS_WRITING_APP": "mkvpropedit v90.0 ('Hanging On') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2025-04-19 10:37:57", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + }, + { + "index": 2, + "codec_name": "mjpeg", + "codec_long_name": "Motion JPEG", + "profile": "Baseline", + "codec_type": "video", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "width": 960, + "height": 540, + "coded_width": 960, + "coded_height": 540, + "closed_captions": 0, + "film_grain": 0, + "has_b_frames": 0, + "sample_aspect_ratio": "1:1", + "display_aspect_ratio": "16:9", + "pix_fmt": "yuvj420p", + "level": -99, + "color_range": "pc", + "color_space": "bt470bg", + "chroma_location": "center", + "refs": 1, + "r_frame_rate": "1000/1", + "avg_frame_rate": "30000/1", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "bits_per_raw_sample": "8", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "non_diegetic": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "BPS": "0", + "DURATION": "00:00:00.000011111", + "NUMBER_OF_FRAMES": "1", + "NUMBER_OF_BYTES": "155034", + "_STATISTICS_WRITING_APP": "mkvpropedit v90.0 ('Hanging On') 64-bit", + "_STATISTICS_WRITING_DATE_UTC": "2025-04-19 10:37:57", + "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" + } + } + ], + "chapters": [], + "format": { + "filename": "file:broken_mkv_covers - 01x03 - statistics added using mkvpropedit.mkv", + "nb_streams": 3, + "nb_programs": 0, + "nb_stream_groups": 0, + "format_name": "matroska,webm", + "format_long_name": "Matroska / WebM", + "start_time": "0.000000", + "duration": "10.005000", + "size": "5425928", + "bit_rate": "4338573", + "probe_score": 100, + "tags": { + "title": "Folge 1: Jackpot Immobilie · Wie wir klug vererben", + "EPISODE_SORT": "1", + "MAJOR_BRAND": "isom", + "MINOR_VERSION": "512", + "COMPATIBLE_BRANDS": "isomiso2avc1mp41", + "DATE": "20250318", + "SEASON_NUMBER": "1", + "COMMENT": "https://www.ardmediathek.de/video/Y3JpZDovL2JyLmRlL2Jyb2FkY2FzdC8zZWFmYTcxOC1mZDJmLTRmZTMtYWE4Ny03ZjdlNWViNTk1NDhfb25saW5lYnJvYWRjYXN0", + "DESCRIPTION": "Jeder kennt es: Enge Familienmitglieder sprechen plötzlich nicht mehr miteinander, der Streit ums Erbe entzweit die Familie. Was steht im Testament? Kann sich die Erbengemeinschaft einigen? Wie läuft das mit der Erbschaftssteuer und was sagt das Erbrecht?\n\nDas \"Lohnt sich das?\"-Team hat zwei Familien gefunden, die das Tabu rund ums Erben brechen. Sie erzählen, um wie viel Geld es geht, aber auch, was dieses Immobilienerbe mit ihrer Familie gemacht hat. Warum es einmal gelingt, Häuser in Millionenhöhe ohne jegliche Erbschaftssteuer zu vererben, während andere Geschwister sich über ihr Elternhaus komplett zerstritten haben. Erbfolge, Freibeträge, Nießbrauch - für alle Basics rund ums Erben ist Ralph Caspers zuständig, der kompetent erklärt. ", + "SYNOPSIS": "Jeder kennt es: Enge Familienmitglieder sprechen plötzlich nicht mehr miteinander, der Streit ums Erbe entzweit die Familie. Was steht im Testament? Kann sich die Erbengemeinschaft einigen? Wie läuft das mit der Erbschaftssteuer und was sagt das Erbrecht?\n\nDas \"Lohnt sich das?\"-Team hat zwei Familien gefunden, die das Tabu rund ums Erben brechen. Sie erzählen, um wie viel Geld es geht, aber auch, was dieses Immobilienerbe mit ihrer Familie gemacht hat. Warum es einmal gelingt, Häuser in Millionenhöhe ohne jegliche Erbschaftssteuer zu vererben, während andere Geschwister sich über ihr Elternhaus komplett zerstritten haben. Erbfolge, Freibeträge, Nießbrauch - für alle Basics rund ums Erben ist Ralph Caspers zuständig, der kompetent erklärt. ", + "SHOW": "Generation Wohnkrise. Lohnt sich das?", + "EPISODE_ID": "Folge 1: Jackpot Immobilie · Wie wir klug vererben", + "ENCODER": "Lavf61.7.100" + } + } +} From 51b54f5695ed86302aefa3c8c15415ce69c7a29b Mon Sep 17 00:00:00 2001 From: MrPlow Date: Sun, 20 Apr 2025 06:08:33 -0400 Subject: [PATCH 209/508] Translated using Weblate (German) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/ --- .../Localization/Core/de.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index c38af5bf43..8eb11ec2bb 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -43,7 +43,7 @@ "NameInstallFailed": "Installation von {0} fehlgeschlagen", "NameSeasonNumber": "Staffel {0}", "NameSeasonUnknown": "Staffel unbekannt", - "NewVersionIsAvailable": "Eine neue Version von Jellyfin-Server steht zum Download bereit.", + "NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.", "NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar", "NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert", "NotificationOptionAudioPlayback": "Audiowiedergabe gestartet", @@ -72,12 +72,12 @@ "ServerNameNeedsToBeRestarted": "{0} muss neu gestartet werden", "Shows": "Serien", "Songs": "Lieder", - "StartupEmbyServerIsLoading": "Jellyfin-Server startet, bitte versuche es gleich noch einmal.", + "StartupEmbyServerIsLoading": "Jellyfin-Server lädt. Bitte versuche es gleich noch einmal.", "SubtitleDownloadFailureForItem": "Download der Untertitel fehlgeschlagen für {0}", "SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden", "Sync": "Synchronisation", "System": "System", - "TvShows": "TV-Sendungen", + "TvShows": "TV-Serien", "User": "Benutzer", "UserCreatedWithName": "Benutzer {0} wurde erstellt", "UserDeletedWithName": "Benutzer {0} wurde gelöscht", @@ -108,14 +108,14 @@ "TaskRefreshLibrary": "Scanne Medien-Bibliothek", "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, die Kapitel besitzen.", "TaskRefreshChapterImages": "Extrahiere Kapitel-Bilder", - "TaskCleanCacheDescription": "Löscht nicht mehr benötigte Zwischenspeicherdateien.", - "TaskCleanCache": "Leere Zwischenspeicher", + "TaskCleanCacheDescription": "Löscht vom System nicht mehr benötigte Zwischenspeicherdateien.", + "TaskCleanCache": "Leere Zwischenspeicher-Verzeichnis", "TasksChannelsCategory": "Internet-Kanäle", "TasksApplicationCategory": "Anwendung", "TasksLibraryCategory": "Bibliothek", "TasksMaintenanceCategory": "Wartung", "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.", - "TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen", + "TaskCleanActivityLog": "Aktivitätsprotokoll löschen", "Undefined": "Undefiniert", "Forced": "Erzwungen", "Default": "Standard", From 576f6d411a5bf5c9a27aa832e9eb56556725f505 Mon Sep 17 00:00:00 2001 From: MrPlow Date: Sun, 20 Apr 2025 06:19:56 -0400 Subject: [PATCH 210/508] Translated using Weblate (German) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/ --- .../Localization/Core/de.json | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 8eb11ec2bb..f5ae43bb5b 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -92,30 +92,30 @@ "ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt", "ValueSpecialEpisodeName": "Extra - {0}", "VersionNumber": "Version {0}", - "TaskDownloadMissingSubtitlesDescription": "Suche im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.", - "TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter", - "TaskRefreshChannelsDescription": "Aktualisiere Internet-Kanal-Informationen.", - "TaskRefreshChannels": "Aktualisiere Kanäle", - "TaskCleanTranscodeDescription": "Löscht Transkodierdateien, die älter als einen Tag sind.", - "TaskCleanTranscode": "Räume Transkodierungs-Verzeichnis auf", + "TaskDownloadMissingSubtitlesDescription": "Sucht im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.", + "TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen", + "TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.", + "TaskRefreshChannels": "Kanäle aktualisieren", + "TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.", + "TaskCleanTranscode": "Transkodierungs-Verzeichnis aufräumen", "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.", - "TaskUpdatePlugins": "Aktualisiere Plugins", + "TaskUpdatePlugins": "Plugins aktualisieren", "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.", - "TaskRefreshPeople": "Aktualisiere Personen", + "TaskRefreshPeople": "Personen aktualisieren", "TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.", - "TaskCleanLogs": "Räumt Log-Verzeichnis auf", - "TaskRefreshLibraryDescription": "Scannt alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiere Metadaten.", - "TaskRefreshLibrary": "Scanne Medien-Bibliothek", + "TaskCleanLogs": "Log-Verzeichnis aufräumen", + "TaskRefreshLibraryDescription": "Durchsucht alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiert Metadaten.", + "TaskRefreshLibrary": "Medien-Bibliothek scannen", "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, die Kapitel besitzen.", - "TaskRefreshChapterImages": "Extrahiere Kapitel-Bilder", + "TaskRefreshChapterImages": "Kapitel-Bilder extrahieren", "TaskCleanCacheDescription": "Löscht vom System nicht mehr benötigte Zwischenspeicherdateien.", - "TaskCleanCache": "Leere Zwischenspeicher-Verzeichnis", + "TaskCleanCache": "Zwischenspeicher-Verzeichnis aufräumen", "TasksChannelsCategory": "Internet-Kanäle", "TasksApplicationCategory": "Anwendung", "TasksLibraryCategory": "Bibliothek", "TasksMaintenanceCategory": "Wartung", "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.", - "TaskCleanActivityLog": "Aktivitätsprotokoll löschen", + "TaskCleanActivityLog": "Aktivitätsprotokolle aufräumen", "Undefined": "Undefiniert", "Forced": "Erzwungen", "Default": "Standard", @@ -128,12 +128,12 @@ "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren", "TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.", "TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen", - "TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.", + "TaskCleanCollectionsAndPlaylistsDescription": "Löscht nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.", "TaskAudioNormalization": "Audio Normalisierung", "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.", "TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter", "TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen", - "TaskExtractMediaSegments": "Scanne Mediensegmente", + "TaskExtractMediaSegments": "Mediensegmente scannen", "TaskExtractMediaSegmentsDescription": "Extrahiert oder empfängt Mediensegmente von Plugins die Mediensegmente nutzen.", "TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren", "TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben." From 5e4bd744c07d44d75c8e9eb7b6dc03b7ff4f147c Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Mon, 21 Apr 2025 03:40:23 +0200 Subject: [PATCH 211/508] Return SyncPlay group info after creation, add GET group endpoint (#13935) --- .../SyncPlay/SyncPlayManager.cs | 28 ++++++++++++++++++- .../Controllers/SyncPlayController.cs | 28 +++++++++++++++---- .../SyncPlay/ISyncPlayManager.cs | 11 +++++++- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index 92b80e1029..b45d754554 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.SyncPlay } /// - public void NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken) + public GroupInfoDto NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken) { if (session is null) { @@ -132,6 +132,7 @@ namespace Emby.Server.Implementations.SyncPlay UpdateSessionsCounter(session.UserId, 1); group.CreateGroup(session, request, cancellationToken); + return group.GetInfo(); } } @@ -288,6 +289,31 @@ namespace Emby.Server.Implementations.SyncPlay return list; } + /// + public GroupInfoDto GetGroup(SessionInfo session, Guid groupId) + { + ArgumentNullException.ThrowIfNull(session); + + var user = _userManager.GetUserById(session.UserId); + + lock (_groupsLock) + { + foreach (var (_, group) in _groups) + { + // Locking required as group is not thread-safe. + lock (group) + { + if (group.GroupId.Equals(groupId) && group.HasAccessToPlayQueue(user)) + { + return group.GetInfo(); + } + } + } + } + + return null; + } + /// public void HandleRequest(SessionInfo session, IGroupPlaybackRequest request, CancellationToken cancellationToken) { diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 3839781971..fbab2a7845 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -1,9 +1,9 @@ +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.SyncPlayDtos; using MediaBrowser.Common.Api; @@ -50,17 +50,16 @@ public class SyncPlayController : BaseJellyfinApiController /// /// The settings of the new group. /// New group created. - /// A indicating success. + /// An for the created group. [HttpPost("New")] - [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.SyncPlayCreateGroup)] - public async Task SyncPlayCreateGroup( + public async Task> SyncPlayCreateGroup( [FromBody, Required] NewGroupRequestDto requestData) { var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); var syncPlayRequest = new NewGroupRequest(requestData.GroupName); - _syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None); - return NoContent(); + return Ok(_syncPlayManager.NewGroup(currentSession, syncPlayRequest, CancellationToken.None)); } /// @@ -112,6 +111,23 @@ public class SyncPlayController : BaseJellyfinApiController return Ok(_syncPlayManager.ListGroups(currentSession, syncPlayRequest).AsEnumerable()); } + /// + /// Gets a SyncPlay group by id. + /// + /// The id of the group. + /// Group returned. + /// An for the requested group. + [HttpGet("{id:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = Policies.SyncPlayJoinGroup)] + public async Task> SyncPlayGetGroup([FromRoute] Guid id) + { + var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); + var group = _syncPlayManager.GetGroup(currentSession, id); + return group == null ? NotFound() : Ok(group); + } + /// /// Request to set new playlist in SyncPlay group. /// diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs index a6999a12c9..6365a389eb 100644 --- a/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayManager.cs @@ -20,7 +20,8 @@ namespace MediaBrowser.Controller.SyncPlay /// The session that's creating the group. /// The request. /// The cancellation token. - void NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken); + /// The newly created group. + GroupInfoDto NewGroup(SessionInfo session, NewGroupRequest request, CancellationToken cancellationToken); /// /// Adds the session to a group. @@ -46,6 +47,14 @@ namespace MediaBrowser.Controller.SyncPlay /// The list of available groups. List ListGroups(SessionInfo session, ListGroupsRequest request); + /// + /// Gets available groups for a session by id. + /// + /// The session. + /// The group id. + /// The groups or null. + GroupInfoDto GetGroup(SessionInfo session, Guid groupId); + /// /// Handle a request by a session in a group. /// From a0931baa8eb879898f4bc4049176ed3bdb4d80d1 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 21 Apr 2025 05:06:50 +0300 Subject: [PATCH 212/508] Add Api and startup check for sufficient storage capacity (#13888) --- Emby.Server.Implementations/SystemManager.cs | 34 ++- Jellyfin.Api/Controllers/SystemController.cs | 14 + .../Models/SystemInfoDtos/FolderStorageDto.cs | 46 ++++ .../SystemInfoDtos/LibraryStorageDto.cs | 37 +++ .../Models/SystemInfoDtos/SystemStorageDto.cs | 67 +++++ .../StorageHelpers/StorageHelper.cs | 109 ++++++++ Jellyfin.Server/Program.cs | 4 + MediaBrowser.Controller/ISystemManager.cs | 7 + .../System/FolderStorageInfo.cs | 32 +++ .../System/LibraryStorageInfo.cs | 25 ++ MediaBrowser.Model/System/SystemInfo.cs | 254 +++++++++--------- .../System/SystemStorageInfo.cs | 56 ++++ 12 files changed, 560 insertions(+), 125 deletions(-) create mode 100644 Jellyfin.Api/Models/SystemInfoDtos/FolderStorageDto.cs create mode 100644 Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs create mode 100644 Jellyfin.Api/Models/SystemInfoDtos/SystemStorageDto.cs create mode 100644 Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs create mode 100644 MediaBrowser.Model/System/FolderStorageInfo.cs create mode 100644 MediaBrowser.Model/System/LibraryStorageInfo.cs create mode 100644 MediaBrowser.Model/System/SystemStorageInfo.cs diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs index 5936df7f1a..92b59b23cd 100644 --- a/Emby.Server.Implementations/SystemManager.cs +++ b/Emby.Server.Implementations/SystemManager.cs @@ -1,9 +1,12 @@ +using System; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Server.Implementations.StorageHelpers; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Updates; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.System; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; @@ -19,6 +22,7 @@ public class SystemManager : ISystemManager private readonly IServerConfigurationManager _configurationManager; private readonly IStartupOptions _startupOptions; private readonly IInstallationManager _installationManager; + private readonly ILibraryManager _libraryManager; /// /// Initializes a new instance of the class. @@ -29,13 +33,15 @@ public class SystemManager : ISystemManager /// Instance of . /// Instance of . /// Instance of . + /// Instance of . public SystemManager( IHostApplicationLifetime applicationLifetime, IServerApplicationHost applicationHost, IServerApplicationPaths applicationPaths, IServerConfigurationManager configurationManager, IStartupOptions startupOptions, - IInstallationManager installationManager) + IInstallationManager installationManager, + ILibraryManager libraryManager) { _applicationLifetime = applicationLifetime; _applicationHost = applicationHost; @@ -43,6 +49,7 @@ public class SystemManager : ISystemManager _configurationManager = configurationManager; _startupOptions = startupOptions; _installationManager = installationManager; + _libraryManager = libraryManager; } /// @@ -57,6 +64,7 @@ public class SystemManager : ISystemManager WebSocketPortNumber = _applicationHost.HttpPort, CompletedInstallations = _installationManager.CompletedInstallations.ToArray(), Id = _applicationHost.SystemId, +#pragma warning disable CS0618 // Type or member is obsolete ProgramDataPath = _applicationPaths.ProgramDataPath, WebPath = _applicationPaths.WebPath, LogPath = _applicationPaths.LogDirectoryPath, @@ -64,6 +72,7 @@ public class SystemManager : ISystemManager InternalMetadataPath = _applicationPaths.InternalMetadataPath, CachePath = _applicationPaths.CachePath, TranscodingTempPath = _configurationManager.GetTranscodePath(), +#pragma warning restore CS0618 // Type or member is obsolete ServerName = _applicationHost.FriendlyName, LocalAddress = _applicationHost.GetSmartApiUrl(request), StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted, @@ -73,6 +82,29 @@ public class SystemManager : ISystemManager }; } + /// + public SystemStorageInfo GetSystemStorageInfo() + { + var virtualFolderInfos = _libraryManager.GetVirtualFolders().Select(e => new LibraryStorageInfo() + { + Id = Guid.Parse(e.ItemId), + Name = e.Name, + Folders = e.Locations.Select(f => StorageHelper.GetFreeSpaceOf(f)).ToArray() + }); + + return new SystemStorageInfo() + { + ProgramDataFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.ProgramDataPath), + WebFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.WebPath), + LogFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.LogDirectoryPath), + ImageCacheFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.ImageCachePath), + InternalMetadataFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.InternalMetadataPath), + CacheFolder = StorageHelper.GetFreeSpaceOf(_applicationPaths.CachePath), + TranscodingTempFolder = StorageHelper.GetFreeSpaceOf(_configurationManager.GetTranscodePath()), + Libraries = virtualFolderInfos.ToArray() + }; + } + /// public PublicSystemInfo GetPublicSystemInfo(HttpRequest request) { diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 0ee11c0704..07a1f76503 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Mime; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using Jellyfin.Api.Models.SystemInfoDtos; using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; @@ -71,6 +72,19 @@ public class SystemController : BaseJellyfinApiController public ActionResult GetSystemInfo() => _systemManager.GetSystemInfo(Request); + /// + /// Gets information about the server. + /// + /// Information retrieved. + /// User does not have permission to retrieve information. + /// A with info about the system. + [HttpGet("Info/Storage")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult GetSystemStorage() + => Ok(SystemStorageDto.FromSystemStorageInfo(_systemManager.GetSystemStorageInfo())); + /// /// Gets public information about the server. /// diff --git a/Jellyfin.Api/Models/SystemInfoDtos/FolderStorageDto.cs b/Jellyfin.Api/Models/SystemInfoDtos/FolderStorageDto.cs new file mode 100644 index 0000000000..00a965898c --- /dev/null +++ b/Jellyfin.Api/Models/SystemInfoDtos/FolderStorageDto.cs @@ -0,0 +1,46 @@ +using MediaBrowser.Model.System; + +namespace Jellyfin.Api.Models.SystemInfoDtos; + +/// +/// Contains information about a specific folder. +/// +public record FolderStorageDto +{ + /// + /// Gets the path of the folder in question. + /// + public required string Path { get; init; } + + /// + /// Gets the free space of the underlying storage device of the . + /// + public long FreeSpace { get; init; } + + /// + /// Gets the used space of the underlying storage device of the . + /// + public long UsedSpace { get; init; } + + /// + /// Gets the kind of storage device of the . + /// + public string? StorageType { get; init; } + + /// + /// Gets the Device Identifier. + /// + public string? DeviceId { get; init; } + + internal static FolderStorageDto FromFolderStorageInfo(FolderStorageInfo model) + { + return new() + { + Path = model.Path, + FreeSpace = model.FreeSpace, + UsedSpace = model.UsedSpace, + StorageType = model.StorageType, + DeviceId = model.DeviceId + }; + } +} diff --git a/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs b/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs new file mode 100644 index 0000000000..c138324d2e --- /dev/null +++ b/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.System; + +namespace Jellyfin.Api.Models.SystemInfoDtos; + +/// +/// Contains informations about a libraries storage informations. +/// +public record LibraryStorageDto +{ + /// + /// Gets or sets the Library Id. + /// + public required Guid Id { get; set; } + + /// + /// Gets or sets the name of the library. + /// + public required string Name { get; set; } + + /// + /// Gets or sets the storage informations about the folders used in a library. + /// + public required IReadOnlyCollection Folders { get; set; } + + internal static LibraryStorageDto FromLibraryStorageModel(LibraryStorageInfo model) + { + return new() + { + Id = model.Id, + Name = model.Name, + Folders = model.Folders.Select(FolderStorageDto.FromFolderStorageInfo).ToArray() + }; + } +} diff --git a/Jellyfin.Api/Models/SystemInfoDtos/SystemStorageDto.cs b/Jellyfin.Api/Models/SystemInfoDtos/SystemStorageDto.cs new file mode 100644 index 0000000000..a09042439a --- /dev/null +++ b/Jellyfin.Api/Models/SystemInfoDtos/SystemStorageDto.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.System; + +namespace Jellyfin.Api.Models.SystemInfoDtos; + +/// +/// Contains informations about the systems storage. +/// +public record SystemStorageDto +{ + /// + /// Gets or sets the Storage information of the program data folder. + /// + public required FolderStorageDto ProgramDataFolder { get; set; } + + /// + /// Gets or sets the Storage information of the web UI resources folder. + /// + public required FolderStorageDto WebFolder { get; set; } + + /// + /// Gets or sets the Storage information of the folder where images are cached. + /// + public required FolderStorageDto ImageCacheFolder { get; set; } + + /// + /// Gets or sets the Storage information of the cache folder. + /// + public required FolderStorageDto CacheFolder { get; set; } + + /// + /// Gets or sets the Storage information of the folder where logfiles are saved to. + /// + public required FolderStorageDto LogFolder { get; set; } + + /// + /// Gets or sets the Storage information of the folder where metadata is stored. + /// + public required FolderStorageDto InternalMetadataFolder { get; set; } + + /// + /// Gets or sets the Storage information of the transcoding cache. + /// + public required FolderStorageDto TranscodingTempFolder { get; set; } + + /// + /// Gets or sets the storage informations of all libraries. + /// + public required IReadOnlyCollection Libraries { get; set; } + + internal static SystemStorageDto FromSystemStorageInfo(SystemStorageInfo model) + { + return new SystemStorageDto() + { + ProgramDataFolder = FolderStorageDto.FromFolderStorageInfo(model.ProgramDataFolder), + WebFolder = FolderStorageDto.FromFolderStorageInfo(model.WebFolder), + ImageCacheFolder = FolderStorageDto.FromFolderStorageInfo(model.ImageCacheFolder), + CacheFolder = FolderStorageDto.FromFolderStorageInfo(model.CacheFolder), + LogFolder = FolderStorageDto.FromFolderStorageInfo(model.LogFolder), + InternalMetadataFolder = FolderStorageDto.FromFolderStorageInfo(model.InternalMetadataFolder), + TranscodingTempFolder = FolderStorageDto.FromFolderStorageInfo(model.TranscodingTempFolder), + Libraries = model.Libraries.Select(LibraryStorageDto.FromLibraryStorageModel).ToArray() + }; + } +} diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs new file mode 100644 index 0000000000..635644179f --- /dev/null +++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs @@ -0,0 +1,109 @@ +using System; +using System.Globalization; +using System.IO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.System; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Implementations.StorageHelpers; + +/// +/// Contains methods to help with checking for storage and returning storage data for jellyfin folders. +/// +public static class StorageHelper +{ + private const long TwoGigabyte = 2_147_483_647L; + private const long FiveHundredAndTwelveMegaByte = 536_870_911L; + private static readonly string[] _byteHumanizedSuffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]; + + /// + /// Tests the available storage capacity on the jellyfin paths with estimated minimum values. + /// + /// The application paths. + /// Logger. + public static void TestCommonPathsForStorageCapacity(IApplicationPaths applicationPaths, ILogger logger) + { + TestDataDirectorySize(applicationPaths.DataPath, logger, TwoGigabyte); + TestDataDirectorySize(applicationPaths.LogDirectoryPath, logger, FiveHundredAndTwelveMegaByte); + TestDataDirectorySize(applicationPaths.CachePath, logger, TwoGigabyte); + TestDataDirectorySize(applicationPaths.ProgramDataPath, logger, TwoGigabyte); + TestDataDirectorySize(applicationPaths.TempDirectory, logger, TwoGigabyte); + } + + /// + /// Gets the free space of a specific directory. + /// + /// Path to a folder. + /// The number of bytes available space. + public static FolderStorageInfo GetFreeSpaceOf(string path) + { + try + { + var driveInfo = new DriveInfo(path); + return new FolderStorageInfo() + { + Path = path, + FreeSpace = driveInfo.AvailableFreeSpace, + UsedSpace = driveInfo.TotalSize - driveInfo.AvailableFreeSpace, + StorageType = driveInfo.DriveType.ToString(), + DeviceId = driveInfo.Name, + }; + } + catch + { + return new FolderStorageInfo() + { + Path = path, + FreeSpace = -1, + UsedSpace = -1, + StorageType = null, + DeviceId = null + }; + } + } + + /// + /// Gets the underlying drive data from a given path and checks if the available storage capacity matches the threshold. + /// + /// The path to a folder to evaluate. + /// The logger. + /// The threshold to check for or -1 to just log the data. + /// Thrown when the threshold is not available on the underlying storage. + private static void TestDataDirectorySize(string path, ILogger logger, long threshold = -1) + { + logger.LogDebug("Check path {TestPath} for storage capacity", path); + var drive = new DriveInfo(path); + if (threshold != -1 && drive.AvailableFreeSpace < threshold) + { + throw new InvalidOperationException($"The path `{path}` has insufficient free space. Required: at least {HumanizeStorageSize(threshold)}."); + } + + logger.LogInformation( + "Storage path `{TestPath}` ({StorageType}) successfully checked with {FreeSpace} free which is over the minimum of {MinFree}.", + path, + drive.DriveType, + HumanizeStorageSize(drive.AvailableFreeSpace), + HumanizeStorageSize(threshold)); + } + + /// + /// Formats a size in bytes into a common human readable form. + /// + /// + /// Taken and slightly modified from https://stackoverflow.com/a/4975942/1786007 . + /// + /// The size in bytes. + /// A human readable approximate representation of the argument. + public static string HumanizeStorageSize(long byteCount) + { + if (byteCount == 0) + { + return $"0{_byteHumanizedSuffixes[0]}"; + } + + var bytes = Math.Abs(byteCount); + var place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); + var num = Math.Round(bytes / Math.Pow(1024, place), 1); + return (Math.Sign(byteCount) * num).ToString(CultureInfo.InvariantCulture) + _byteHumanizedSuffixes[place]; + } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 55a4a00878..8d0bf73f6a 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -11,6 +12,7 @@ using Emby.Server.Implementations; using Jellyfin.Database.Implementations; using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; +using Jellyfin.Server.Implementations.StorageHelpers; using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; @@ -120,6 +122,8 @@ namespace Jellyfin.Server } } + StorageHelper.TestCommonPathsForStorageCapacity(appPaths, _loggerFactory.CreateLogger()); + StartupHelpers.PerformStaticInitialization(); await Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory).ConfigureAwait(false); diff --git a/MediaBrowser.Controller/ISystemManager.cs b/MediaBrowser.Controller/ISystemManager.cs index ef3034d2f5..08344a1e51 100644 --- a/MediaBrowser.Controller/ISystemManager.cs +++ b/MediaBrowser.Controller/ISystemManager.cs @@ -1,5 +1,6 @@ using MediaBrowser.Model.System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; namespace MediaBrowser.Controller; @@ -31,4 +32,10 @@ public interface ISystemManager /// Starts the application shutdown process. /// void Shutdown(); + + /// + /// Gets the systems storage resources. + /// + /// The . + SystemStorageInfo GetSystemStorageInfo(); } diff --git a/MediaBrowser.Model/System/FolderStorageInfo.cs b/MediaBrowser.Model/System/FolderStorageInfo.cs new file mode 100644 index 0000000000..7b10e4ea58 --- /dev/null +++ b/MediaBrowser.Model/System/FolderStorageInfo.cs @@ -0,0 +1,32 @@ +namespace MediaBrowser.Model.System; + +/// +/// Contains information about a specific folder. +/// +public record FolderStorageInfo +{ + /// + /// Gets the path of the folder in question. + /// + public required string Path { get; init; } + + /// + /// Gets the free space of the underlying storage device of the . + /// + public long FreeSpace { get; init; } + + /// + /// Gets the used space of the underlying storage device of the . + /// + public long UsedSpace { get; init; } + + /// + /// Gets the kind of storage device of the . + /// + public string? StorageType { get; init; } + + /// + /// Gets the Device Identifier. + /// + public string? DeviceId { get; init; } +} diff --git a/MediaBrowser.Model/System/LibraryStorageInfo.cs b/MediaBrowser.Model/System/LibraryStorageInfo.cs new file mode 100644 index 0000000000..d4111b29c7 --- /dev/null +++ b/MediaBrowser.Model/System/LibraryStorageInfo.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Model.System; + +/// +/// Contains informations about a libraries storage informations. +/// +public class LibraryStorageInfo +{ + /// + /// Gets or sets the Library Id. + /// + public required Guid Id { get; set; } + + /// + /// Gets or sets the name of the library. + /// + public required string Name { get; set; } + + /// + /// Gets or sets the storage informations about the folders used in a library. + /// + public required IReadOnlyCollection Folders { get; set; } +} diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index f37ac6a147..232a2a6bc8 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -6,133 +6,139 @@ using System.Collections.Generic; using System.ComponentModel; using MediaBrowser.Model.Updates; -namespace MediaBrowser.Model.System +namespace MediaBrowser.Model.System; + +/// +/// Class SystemInfo. +/// +public class SystemInfo : PublicSystemInfo { /// - /// Class SystemInfo. + /// Initializes a new instance of the class. /// - public class SystemInfo : PublicSystemInfo + public SystemInfo() { - /// - /// Initializes a new instance of the class. - /// - public SystemInfo() - { - CompletedInstallations = Array.Empty(); - } - - /// - /// Gets or sets the display name of the operating system. - /// - /// The display name of the operating system. - [Obsolete("This is no longer set")] - public string OperatingSystemDisplayName { get; set; } = string.Empty; - - /// - /// Gets or sets the package name. - /// - /// The value of the '-package' command line argument. - public string PackageName { get; set; } - - /// - /// Gets or sets a value indicating whether this instance has pending restart. - /// - /// true if this instance has pending restart; otherwise, false. - public bool HasPendingRestart { get; set; } - - public bool IsShuttingDown { get; set; } - - /// - /// Gets or sets a value indicating whether [supports library monitor]. - /// - /// true if [supports library monitor]; otherwise, false. - public bool SupportsLibraryMonitor { get; set; } - - /// - /// Gets or sets the web socket port number. - /// - /// The web socket port number. - public int WebSocketPortNumber { get; set; } - - /// - /// Gets or sets the completed installations. - /// - /// The completed installations. - public InstallationInfo[] CompletedInstallations { get; set; } - - /// - /// Gets or sets a value indicating whether this instance can self restart. - /// - /// true. - [Obsolete("This is always true")] - [DefaultValue(true)] - public bool CanSelfRestart { get; set; } = true; - - [Obsolete("This is always false")] - [DefaultValue(false)] - public bool CanLaunchWebBrowser { get; set; } = false; - - /// - /// Gets or sets the program data path. - /// - /// The program data path. - public string ProgramDataPath { get; set; } - - /// - /// Gets or sets the web UI resources path. - /// - /// The web UI resources path. - public string WebPath { get; set; } - - /// - /// Gets or sets the items by name path. - /// - /// The items by name path. - public string ItemsByNamePath { get; set; } - - /// - /// Gets or sets the cache path. - /// - /// The cache path. - public string CachePath { get; set; } - - /// - /// Gets or sets the log path. - /// - /// The log path. - public string LogPath { get; set; } - - /// - /// Gets or sets the internal metadata path. - /// - /// The internal metadata path. - public string InternalMetadataPath { get; set; } - - /// - /// Gets or sets the transcode path. - /// - /// The transcode path. - public string TranscodingTempPath { get; set; } - - /// - /// Gets or sets the list of cast receiver applications. - /// - public IReadOnlyList CastReceiverApplications { get; set; } - - /// - /// Gets or sets a value indicating whether this instance has update available. - /// - /// true if this instance has update available; otherwise, false. - [Obsolete("This should be handled by the package manager")] - [DefaultValue(false)] - public bool HasUpdateAvailable { get; set; } - - [Obsolete("This isn't set correctly anymore")] - [DefaultValue("System")] - public string EncoderLocation { get; set; } = "System"; - - [Obsolete("This is no longer set")] - [DefaultValue("X64")] - public string SystemArchitecture { get; set; } = "X64"; + CompletedInstallations = Array.Empty(); } + + /// + /// Gets or sets the display name of the operating system. + /// + /// The display name of the operating system. + [Obsolete("This is no longer set")] + public string OperatingSystemDisplayName { get; set; } = string.Empty; + + /// + /// Gets or sets the package name. + /// + /// The value of the '-package' command line argument. + public string PackageName { get; set; } + + /// + /// Gets or sets a value indicating whether this instance has pending restart. + /// + /// true if this instance has pending restart; otherwise, false. + public bool HasPendingRestart { get; set; } + + public bool IsShuttingDown { get; set; } + + /// + /// Gets or sets a value indicating whether [supports library monitor]. + /// + /// true if [supports library monitor]; otherwise, false. + public bool SupportsLibraryMonitor { get; set; } + + /// + /// Gets or sets the web socket port number. + /// + /// The web socket port number. + public int WebSocketPortNumber { get; set; } + + /// + /// Gets or sets the completed installations. + /// + /// The completed installations. + public InstallationInfo[] CompletedInstallations { get; set; } + + /// + /// Gets or sets a value indicating whether this instance can self restart. + /// + /// true. + [Obsolete("This is always true")] + [DefaultValue(true)] + public bool CanSelfRestart { get; set; } = true; + + [Obsolete("This is always false")] + [DefaultValue(false)] + public bool CanLaunchWebBrowser { get; set; } = false; + + /// + /// Gets or sets the program data path. + /// + /// The program data path. + [Obsolete("Use the newer SystemStorageDto instead")] + public string ProgramDataPath { get; set; } + + /// + /// Gets or sets the web UI resources path. + /// + /// The web UI resources path. + [Obsolete("Use the newer SystemStorageDto instead")] + public string WebPath { get; set; } + + /// + /// Gets or sets the items by name path. + /// + /// The items by name path. + [Obsolete("Use the newer SystemStorageDto instead")] + public string ItemsByNamePath { get; set; } + + /// + /// Gets or sets the cache path. + /// + /// The cache path. + [Obsolete("Use the newer SystemStorageDto instead")] + public string CachePath { get; set; } + + /// + /// Gets or sets the log path. + /// + /// The log path. + [Obsolete("Use the newer SystemStorageDto instead")] + public string LogPath { get; set; } + + /// + /// Gets or sets the internal metadata path. + /// + /// The internal metadata path. + [Obsolete("Use the newer SystemStorageDto instead")] + public string InternalMetadataPath { get; set; } + + /// + /// Gets or sets the transcode path. + /// + /// The transcode path. + [Obsolete("Use the newer SystemStorageDto instead")] + public string TranscodingTempPath { get; set; } + + /// + /// Gets or sets the list of cast receiver applications. + /// + public IReadOnlyList CastReceiverApplications { get; set; } + + /// + /// Gets or sets a value indicating whether this instance has update available. + /// + /// true if this instance has update available; otherwise, false. + [Obsolete("This should be handled by the package manager")] + [DefaultValue(false)] + public bool HasUpdateAvailable { get; set; } + + [Obsolete("This isn't set correctly anymore")] + [DefaultValue("System")] + public string EncoderLocation { get; set; } = "System"; + + [Obsolete("This is no longer set")] + [DefaultValue("X64")] + public string SystemArchitecture { get; set; } = "X64"; } diff --git a/MediaBrowser.Model/System/SystemStorageInfo.cs b/MediaBrowser.Model/System/SystemStorageInfo.cs new file mode 100644 index 0000000000..42e7a37e02 --- /dev/null +++ b/MediaBrowser.Model/System/SystemStorageInfo.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Model.System; + +/// +/// Contains informations about the systems storage. +/// +public class SystemStorageInfo +{ + /// + /// Gets or sets the program data path. + /// + /// The program data path. + public required FolderStorageInfo ProgramDataFolder { get; set; } + + /// + /// Gets or sets the web UI resources path. + /// + /// The web UI resources path. + public required FolderStorageInfo WebFolder { get; set; } + + /// + /// Gets or sets the items by name path. + /// + /// The items by name path. + public required FolderStorageInfo ImageCacheFolder { get; set; } + + /// + /// Gets or sets the cache path. + /// + /// The cache path. + public required FolderStorageInfo CacheFolder { get; set; } + + /// + /// Gets or sets the log path. + /// + /// The log path. + public required FolderStorageInfo LogFolder { get; set; } + + /// + /// Gets or sets the internal metadata path. + /// + /// The internal metadata path. + public required FolderStorageInfo InternalMetadataFolder { get; set; } + + /// + /// Gets or sets the transcode path. + /// + /// The transcode path. + public required FolderStorageInfo TranscodingTempFolder { get; set; } + + /// + /// Gets or sets the storage informations of all libraries. + /// + public required IReadOnlyCollection Libraries { get; set; } +} From 1dd38081470cbb61842728ffea92e2a3bbc80271 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 17:34:25 +0000 Subject: [PATCH 213/508] Update dependency z440.atl.core to 6.21.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 02e8c146dc..cdce608d9c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -80,7 +80,7 @@ - + From 32887adff3a01e5ae77b35261908d5c84dd37d41 Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Wed, 23 Apr 2025 22:00:07 +0200 Subject: [PATCH 214/508] Deprecate OnPlaybackXXX API operations in favor of ReportPlaybackXXX --- Jellyfin.Api/Controllers/PlaystateController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index 1577b45947..ade0906b34 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -272,6 +272,7 @@ public class PlaystateController : BaseJellyfinApiController /// A . [HttpPost("PlayingItems/{itemId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Obsolete("This endpoint is obsolete. Use ReportPlaybackStart instead")] public async Task OnPlaybackStart( [FromRoute, Required] Guid itemId, [FromQuery] string? mediaSourceId, @@ -350,6 +351,7 @@ public class PlaystateController : BaseJellyfinApiController /// A . [HttpPost("PlayingItems/{itemId}/Progress")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Obsolete("This endpoint is obsolete. Use ReportPlaybackProgress instead")] public async Task OnPlaybackProgress( [FromRoute, Required] Guid itemId, [FromQuery] string? mediaSourceId, @@ -438,6 +440,7 @@ public class PlaystateController : BaseJellyfinApiController /// A . [HttpDelete("PlayingItems/{itemId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [Obsolete("This endpoint is obsolete. Use ReportPlaybackStop instead")] public async Task OnPlaybackStopped( [FromRoute, Required] Guid itemId, [FromQuery] string? mediaSourceId, From c4cb41f3b23386b6391f035f12607ffe4f6ef2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Thu, 24 Apr 2025 05:31:03 +0200 Subject: [PATCH 215/508] Temporarily ignore NonTransactionalMigrationOperation warnings (#13972) --- .../SqliteDatabaseProvider.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index bd5631074f..927ba63b97 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -7,6 +7,7 @@ using Jellyfin.Database.Implementations; using MediaBrowser.Common.Configuration; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; namespace Jellyfin.Database.Providers.Sqlite; @@ -38,9 +39,13 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider /// public void Initialise(DbContextOptionsBuilder options) { - options.UseSqlite( - $"Filename={Path.Combine(_applicationPaths.DataPath, "jellyfin.db")};Pooling=false", - sqLiteOptions => sqLiteOptions.MigrationsAssembly(GetType().Assembly)); + options + .UseSqlite( + $"Filename={Path.Combine(_applicationPaths.DataPath, "jellyfin.db")};Pooling=false", + sqLiteOptions => sqLiteOptions.MigrationsAssembly(GetType().Assembly)) + // TODO: Remove when https://github.com/dotnet/efcore/pull/35873 is merged & released + .ConfigureWarnings(warnings => + warnings.Ignore(RelationalEventId.NonTransactionalMigrationOperationWarning)); } /// From 317d76e97c08d52617c492474e0987af073b5d06 Mon Sep 17 00:00:00 2001 From: Teja Chundru Date: Wed, 23 Apr 2025 15:29:45 -0400 Subject: [PATCH 216/508] Translated using Weblate (Telugu) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/te/ --- Emby.Server.Implementations/Localization/Core/te.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json index 7d4422d62f..1fa2a3cc54 100644 --- a/Emby.Server.Implementations/Localization/Core/te.json +++ b/Emby.Server.Implementations/Localization/Core/te.json @@ -51,5 +51,13 @@ "Latest": "తాజా", "NameInstallFailed": "{0} ఇన్‌స్టాలేషన్ విఫలమైంది", "NameSeasonUnknown": "భాగం తెలియదు", - "NotificationOptionApplicationUpdateAvailable": "అప్లికేషన్ అప్‌డేట్ అందుబాటులో ఉంది" + "NotificationOptionApplicationUpdateAvailable": "అప్లికేషన్ అప్‌డేట్ అందుబాటులో ఉంది", + "NameSeasonNumber": "సీజన్ {0}", + "NotificationOptionAudioPlaybackStopped": "ఆడియో ఆడటం ఆగిపోయింది", + "NotificationOptionNewLibraryContent": "కొత్త కంటెంట్ జోడించబడింది", + "MixedContent": "వివిధ రకాల కంటెంట్", + "NotificationOptionAudioPlayback": "ఆడియో ప్లే కావడం మొదలైంది", + "NotificationOptionCameraImageUploaded": "కెమెరా చిత్రాన్ని అప్లోడ్ చేశారు", + "NotificationOptionInstallationFailed": "ఇన్స్టాలేషన్ విఫలమైంది", + "NotificationOptionServerRestartRequired": "సర్వర్ రీస్టార్ట్ అవసరం" } From cf0220f183968fa1e11d8a3fd46f709883b7bb7a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:06:11 +0000 Subject: [PATCH 217/508] Update CI dependencies --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/ci-openapi.yml | 8 ++++---- .github/workflows/commands.yml | 2 +- .github/workflows/issue-template-check.yml | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 1eced1913b..60254af71c 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 + uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 + uses: github/codeql-action/autobuild@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 + uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 13b029e52c..b3be082e0d 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -85,13 +85,13 @@ jobs: steps: - name: Download abi-head - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: abi-head path: abi-head - name: Download abi-base - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: abi-base path: abi-base diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 95e090f9b6..abfdf71f6e 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -80,12 +80,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 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@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: openapi-head path: openapi-head @@ -220,7 +220,7 @@ jobs: run: |- echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: openapi-head path: openapi-head diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 5ec4d164a5..374554ac07 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -44,7 +44,7 @@ jobs: with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.13' cache: 'pip' diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml index 8a21ab0151..588f0a50e8 100644 --- a/.github/workflows/issue-template-check.yml +++ b/.github/workflows/issue-template-check.yml @@ -14,7 +14,7 @@ jobs: with: repository: jellyfin/jellyfin-triage-script - name: install python - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.13' cache: 'pip' From df5671263fc8370ae17b7a5d53f06a86de5cbc93 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 26 Apr 2025 14:01:12 +0200 Subject: [PATCH 218/508] Merge pull request #13847 from Shadowghost/rework-chapter-management Rework chapter management --- .../ApplicationHost.cs | 5 +- .../Chapters/ChapterManager.cs | 313 ++++++++++++++++++ Emby.Server.Implementations/Dto/DtoService.cs | 9 +- .../Library/PathManager.cs | 20 +- .../MediaEncoder/EncodingManager.cs | 272 --------------- .../ScheduledTasks/Tasks/ChapterImagesTask.cs | 28 +- .../Item/ChapterRepository.cs | 30 +- .../Chapters/IChapterManager.cs | 55 +++ MediaBrowser.Controller/Entities/BaseItem.cs | 7 +- MediaBrowser.Controller/IO/IPathManager.cs | 16 +- .../MediaEncoding/IEncodingManager.cs | 28 -- .../IChapterRepository.cs | 26 +- .../MediaInfo/AudioFileProber.cs | 4 - .../MediaInfo/FFProbeVideoInfo.cs | 16 +- .../MediaInfo/ProbeProvider.cs | 13 +- 15 files changed, 447 insertions(+), 395 deletions(-) create mode 100644 Emby.Server.Implementations/Chapters/ChapterManager.cs delete mode 100644 Emby.Server.Implementations/MediaEncoder/EncodingManager.cs create mode 100644 MediaBrowser.Controller/Chapters/IChapterManager.cs delete mode 100644 MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs rename MediaBrowser.Controller/{Chapters => Persistence}/IChapterRepository.cs (60%) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 5bb75e2b95..7b07243da7 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -15,6 +15,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Emby.Naming.Common; using Emby.Photos; +using Emby.Server.Implementations.Chapters; using Emby.Server.Implementations.Collections; using Emby.Server.Implementations.Configuration; using Emby.Server.Implementations.Cryptography; @@ -552,7 +553,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -647,7 +648,7 @@ namespace Emby.Server.Implementations BaseItem.ProviderManager = Resolve(); BaseItem.LocalizationManager = Resolve(); BaseItem.ItemRepository = Resolve(); - BaseItem.ChapterRepository = Resolve(); + BaseItem.ChapterManager = Resolve(); BaseItem.FileSystem = Resolve(); BaseItem.UserDataManager = Resolve(); BaseItem.ChannelManager = Resolve(); diff --git a/Emby.Server.Implementations/Chapters/ChapterManager.cs b/Emby.Server.Implementations/Chapters/ChapterManager.cs new file mode 100644 index 0000000000..b4daa2a143 --- /dev/null +++ b/Emby.Server.Implementations/Chapters/ChapterManager.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Chapters; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Chapters; + +/// +/// The chapter manager. +/// +public class ChapterManager : IChapterManager +{ + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + private readonly IMediaEncoder _encoder; + private readonly IChapterRepository _chapterRepository; + private readonly ILibraryManager _libraryManager; + private readonly IPathManager _pathManager; + + /// + /// The first chapter ticks. + /// + private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks; + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The . + /// The . + /// The . + /// The . + /// The . + public ChapterManager( + ILogger logger, + IFileSystem fileSystem, + IMediaEncoder encoder, + IChapterRepository chapterRepository, + ILibraryManager libraryManager, + IPathManager pathManager) + { + _logger = logger; + _fileSystem = fileSystem; + _encoder = encoder; + _chapterRepository = chapterRepository; + _libraryManager = libraryManager; + _pathManager = pathManager; + } + + /// + /// Determines whether [is eligible for chapter image extraction] [the specified video]. + /// + /// The video. + /// The library options for the video. + /// true if [is eligible for chapter image extraction] [the specified video]; otherwise, false. + private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions) + { + if (video.IsPlaceHolder) + { + return false; + } + + if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction) + { + return false; + } + + if (video.IsShortcut) + { + return false; + } + + if (!video.IsCompleteMedia) + { + return false; + } + + // Can't extract images if there are no video streams + return video.DefaultVideoStreamIndex.HasValue; + } + + private long GetAverageDurationBetweenChapters(IReadOnlyList chapters) + { + if (chapters.Count < 2) + { + return 0; + } + + long sum = 0; + for (int i = 1; i < chapters.Count; i++) + { + sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks; + } + + return sum / chapters.Count; + } + + /// + public async Task RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) + { + if (chapters.Count == 0) + { + return true; + } + + var libraryOptions = _libraryManager.GetLibraryOptions(video); + + if (!IsEligibleForChapterImageExtraction(video, libraryOptions)) + { + extractImages = false; + } + + var averageChapterDuration = GetAverageDurationBetweenChapters(chapters); + var threshold = TimeSpan.FromSeconds(1).Ticks; + if (averageChapterDuration < threshold) + { + _logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold); + extractImages = false; + } + + var success = true; + var changesMade = false; + + var runtimeTicks = video.RunTimeTicks ?? 0; + + var currentImages = GetSavedChapterImages(video, directoryService); + + foreach (var chapter in chapters) + { + if (chapter.StartPositionTicks >= runtimeTicks) + { + _logger.LogInformation("Stopping chapter extraction for {0} because a chapter was found with a position greater than the runtime.", video.Name); + break; + } + + var path = _pathManager.GetChapterImagePath(video, chapter.StartPositionTicks); + + if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase)) + { + if (extractImages) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + // Add some time for the first chapter to make sure we don't end up with a black image + var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); + + var inputPath = video.Path; + var directoryPath = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + var container = video.Container; + var mediaSource = new MediaSourceInfo + { + VideoType = video.VideoType, + IsoType = video.IsoType, + Protocol = video.PathProtocol ?? MediaProtocol.File, + }; + + _logger.LogInformation("Extracting chapter image for {Name} at {Path}", video.Name, inputPath); + var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); + File.Copy(tempFile, path, true); + + try + { + _fileSystem.DeleteFile(tempFile); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile); + } + + chapter.ImagePath = path; + chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); + changesMade = true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path)); + success = false; + break; + } + } + else if (!string.IsNullOrEmpty(chapter.ImagePath)) + { + chapter.ImagePath = null; + changesMade = true; + } + } + else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase)) + { + chapter.ImagePath = path; + chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); + changesMade = true; + } + else if (libraryOptions?.EnableChapterImageExtraction != true) + { + // We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image + chapter.ImagePath = null; + changesMade = true; + } + } + + if (saveChapters && changesMade) + { + _chapterRepository.SaveChapters(video.Id, chapters); + } + + DeleteDeadImages(currentImages, chapters); + + return success; + } + + /// + public void SaveChapters(Video video, IReadOnlyList chapters) + { + _chapterRepository.SaveChapters(video.Id, chapters); + } + + /// + public ChapterInfo? GetChapter(Guid baseItemId, int index) + { + return _chapterRepository.GetChapter(baseItemId, index); + } + + /// + public IReadOnlyList GetChapters(Guid baseItemId) + { + return _chapterRepository.GetChapters(baseItemId); + } + + /// + public void DeleteChapterImages(Video video) + { + var path = _pathManager.GetChapterImageFolderPath(video); + try + { + if (Directory.Exists(path)) + { + _logger.LogInformation("Removing chapter images for {Name} [{Id}]", video.Name, video.Id); + Directory.Delete(path, true); + } + } + catch (Exception ex) + { + _logger.LogWarning("Failed to remove chapter image folder for {Item}: {Exception}", video.Id, ex); + } + + _chapterRepository.DeleteChapters(video.Id); + } + + private IReadOnlyList GetSavedChapterImages(Video video, IDirectoryService directoryService) + { + var path = _pathManager.GetChapterImageFolderPath(video); + if (!Directory.Exists(path)) + { + return []; + } + + try + { + return directoryService.GetFilePaths(path); + } + catch (IOException) + { + return []; + } + } + + private void DeleteDeadImages(IEnumerable images, IEnumerable chapters) + { + var existingImages = chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)); + var deadImages = images + .Except(existingImages, StringComparer.OrdinalIgnoreCase) + .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase)) + .ToList(); + + foreach (var image in deadImages) + { + _logger.LogDebug("Deleting dead chapter image {Path}", image); + + try + { + _fileSystem.DeleteFile(image!); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting {Path}.", image); + } + } + } +} diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 5b0fc9ef3b..9e0a6080d3 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -17,7 +17,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Trickplay; @@ -51,7 +50,7 @@ namespace Emby.Server.Implementations.Dto private readonly Lazy _livetvManagerFactory; private readonly ITrickplayManager _trickplayManager; - private readonly IChapterRepository _chapterRepository; + private readonly IChapterManager _chapterManager; public DtoService( ILogger logger, @@ -64,7 +63,7 @@ namespace Emby.Server.Implementations.Dto IMediaSourceManager mediaSourceManager, Lazy livetvManagerFactory, ITrickplayManager trickplayManager, - IChapterRepository chapterRepository) + IChapterManager chapterManager) { _logger = logger; _libraryManager = libraryManager; @@ -76,7 +75,7 @@ namespace Emby.Server.Implementations.Dto _mediaSourceManager = mediaSourceManager; _livetvManagerFactory = livetvManagerFactory; _trickplayManager = trickplayManager; - _chapterRepository = chapterRepository; + _chapterManager = chapterManager; } private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; @@ -1061,7 +1060,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.Chapters)) { - dto.Chapters = _chapterRepository.GetChapters(item.Id).ToList(); + dto.Chapters = _chapterManager.GetChapters(item.Id).ToList(); } if (options.ContainsField(ItemFields.Trickplay)) diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs index 83a6df9644..dbd2333ff3 100644 --- a/Emby.Server.Implementations/Library/PathManager.cs +++ b/Emby.Server.Implementations/Library/PathManager.cs @@ -29,9 +29,9 @@ public class PathManager : IPathManager _appPaths = appPaths; } - private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); + private string SubtitleCachePath => Path.Join(_appPaths.DataPath, "subtitles"); - private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments"); + private string AttachmentCachePath => Path.Join(_appPaths.DataPath, "attachments"); /// public string GetAttachmentPath(string mediaSourceId, string fileName) @@ -67,7 +67,21 @@ public class PathManager : IPathManager var id = item.Id.ToString("D", CultureInfo.InvariantCulture).AsSpan(); return saveWithMedia - ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) + ? Path.Join(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) : Path.Join(_config.ApplicationPaths.TrickplayPath, id[..2], id); } + + /// + public string GetChapterImageFolderPath(BaseItem item) + { + return Path.Join(item.GetInternalMetadataPath(), "chapters"); + } + + /// + public string GetChapterImagePath(BaseItem item, long chapterPositionTicks) + { + var filename = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg"; + + return Path.Join(GetChapterImageFolderPath(item), filename); + } } diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs deleted file mode 100644 index ea78968617..0000000000 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ /dev/null @@ -1,272 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Extensions; -using MediaBrowser.Controller.Chapters; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.MediaEncoder -{ - public class EncodingManager : IEncodingManager - { - private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; - private readonly IMediaEncoder _encoder; - private readonly IChapterRepository _chapterManager; - private readonly ILibraryManager _libraryManager; - - /// - /// The first chapter ticks. - /// - private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks; - - public EncodingManager( - ILogger logger, - IFileSystem fileSystem, - IMediaEncoder encoder, - IChapterRepository chapterManager, - ILibraryManager libraryManager) - { - _logger = logger; - _fileSystem = fileSystem; - _encoder = encoder; - _chapterManager = chapterManager; - _libraryManager = libraryManager; - } - - /// - /// Gets the chapter images data path. - /// - /// The chapter images data path. - private static string GetChapterImagesPath(BaseItem item) - { - return Path.Combine(item.GetInternalMetadataPath(), "chapters"); - } - - /// - /// Determines whether [is eligible for chapter image extraction] [the specified video]. - /// - /// The video. - /// The library options for the video. - /// true if [is eligible for chapter image extraction] [the specified video]; otherwise, false. - private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions) - { - if (video.IsPlaceHolder) - { - return false; - } - - if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction) - { - return false; - } - - if (video.IsShortcut) - { - return false; - } - - if (!video.IsCompleteMedia) - { - return false; - } - - // Can't extract images if there are no video streams - return video.DefaultVideoStreamIndex.HasValue; - } - - private long GetAverageDurationBetweenChapters(IReadOnlyList chapters) - { - if (chapters.Count < 2) - { - return 0; - } - - long sum = 0; - for (int i = 1; i < chapters.Count; i++) - { - sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks; - } - - return sum / chapters.Count; - } - - public async Task RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) - { - if (chapters.Count == 0) - { - return true; - } - - var libraryOptions = _libraryManager.GetLibraryOptions(video); - - if (!IsEligibleForChapterImageExtraction(video, libraryOptions)) - { - extractImages = false; - } - - var averageChapterDuration = GetAverageDurationBetweenChapters(chapters); - var threshold = TimeSpan.FromSeconds(1).Ticks; - if (averageChapterDuration < threshold) - { - _logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold); - extractImages = false; - } - - var success = true; - var changesMade = false; - - var runtimeTicks = video.RunTimeTicks ?? 0; - - var currentImages = GetSavedChapterImages(video, directoryService); - - foreach (var chapter in chapters) - { - if (chapter.StartPositionTicks >= runtimeTicks) - { - _logger.LogInformation("Stopping chapter extraction for {0} because a chapter was found with a position greater than the runtime.", video.Name); - break; - } - - var path = GetChapterImagePath(video, chapter.StartPositionTicks); - - if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase)) - { - if (extractImages) - { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - // Add some time for the first chapter to make sure we don't end up with a black image - var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); - - var inputPath = video.Path; - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - var container = video.Container; - var mediaSource = new MediaSourceInfo - { - VideoType = video.VideoType, - IsoType = video.IsoType, - Protocol = video.PathProtocol.Value, - }; - - var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); - File.Copy(tempFile, path, true); - - try - { - _fileSystem.DeleteFile(tempFile); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile); - } - - chapter.ImagePath = path; - chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); - changesMade = true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path)); - success = false; - break; - } - } - else if (!string.IsNullOrEmpty(chapter.ImagePath)) - { - chapter.ImagePath = null; - changesMade = true; - } - } - else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase)) - { - chapter.ImagePath = path; - chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); - changesMade = true; - } - else if (libraryOptions?.EnableChapterImageExtraction != true) - { - // We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image - chapter.ImagePath = null; - changesMade = true; - } - } - - if (saveChapters && changesMade) - { - _chapterManager.SaveChapters(video.Id, chapters); - } - - DeleteDeadImages(currentImages, chapters); - - return success; - } - - private string GetChapterImagePath(Video video, long chapterPositionTicks) - { - var filename = video.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg"; - - return Path.Combine(GetChapterImagesPath(video), filename); - } - - private static IReadOnlyList GetSavedChapterImages(Video video, IDirectoryService directoryService) - { - var path = GetChapterImagesPath(video); - if (!Directory.Exists(path)) - { - return Array.Empty(); - } - - try - { - return directoryService.GetFilePaths(path); - } - catch (IOException) - { - return Array.Empty(); - } - } - - private void DeleteDeadImages(IEnumerable images, IEnumerable chapters) - { - var deadImages = images - .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase) - .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase)) - .ToList(); - - foreach (var image in deadImages) - { - _logger.LogDebug("Deleting dead chapter image {Path}", image); - - try - { - _fileSystem.DeleteFile(image); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting {Path}.", image); - } - } - } - } -} diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 563e90fbea..b76fdeeb04 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -11,8 +11,6 @@ using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; @@ -28,42 +26,34 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks { private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; - private readonly IItemRepository _itemRepo; private readonly IApplicationPaths _appPaths; - private readonly IEncodingManager _encodingManager; + private readonly IChapterManager _chapterManager; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; - private readonly IChapterRepository _chapterRepository; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. + /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. public ChapterImagesTask( ILogger logger, ILibraryManager libraryManager, - IItemRepository itemRepo, IApplicationPaths appPaths, - IEncodingManager encodingManager, + IChapterManager chapterManager, IFileSystem fileSystem, - ILocalizationManager localization, - IChapterRepository chapterRepository) + ILocalizationManager localization) { _logger = logger; _libraryManager = libraryManager; - _itemRepo = itemRepo; _appPaths = appPaths; - _encodingManager = encodingManager; + _chapterManager = chapterManager; _fileSystem = fileSystem; _localization = localization; - _chapterRepository = chapterRepository; } /// @@ -126,12 +116,12 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } catch (IOException) { - previouslyFailedImages = new List(); + previouslyFailedImages = []; } } else { - previouslyFailedImages = new List(); + previouslyFailedImages = []; } var directoryService = new DirectoryService(_fileSystem); @@ -146,9 +136,9 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks try { - var chapters = _chapterRepository.GetChapters(video.Id); + var chapters = _chapterManager.GetChapters(video.Id); - var success = await _encodingManager.RefreshChapterImages(video, directoryService, chapters, extract, true, cancellationToken).ConfigureAwait(false); + var success = await _chapterManager.RefreshChapterImages(video, directoryService, chapters, extract, true, cancellationToken).ConfigureAwait(false); if (!success) { diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs index 93e15735c9..9f2d473468 100644 --- a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs +++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; -using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Drawing; -using MediaBrowser.Model.Dto; +using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using Microsoft.EntityFrameworkCore; @@ -31,19 +29,7 @@ public class ChapterRepository : IChapterRepository _imageProcessor = imageProcessor; } - /// - public ChapterInfo? GetChapter(BaseItemDto baseItem, int index) - { - return GetChapter(baseItem.Id, index); - } - - /// - public IReadOnlyList GetChapters(BaseItemDto baseItem) - { - return GetChapters(baseItem.Id); - } - - /// + /// public ChapterInfo? GetChapter(Guid baseItemId, int index) { using var context = _dbProvider.CreateDbContext(); @@ -62,7 +48,7 @@ public class ChapterRepository : IChapterRepository return null; } - /// + /// public IReadOnlyList GetChapters(Guid baseItemId) { using var context = _dbProvider.CreateDbContext(); @@ -77,7 +63,7 @@ public class ChapterRepository : IChapterRepository .ToArray(); } - /// + /// public void SaveChapters(Guid itemId, IReadOnlyList chapters) { using var context = _dbProvider.CreateDbContext(); @@ -95,6 +81,14 @@ public class ChapterRepository : IChapterRepository } } + /// + public void DeleteChapters(Guid itemId) + { + using var context = _dbProvider.CreateDbContext(); + context.Chapters.Where(c => c.ItemId.Equals(itemId)).ExecuteDelete(); + context.SaveChanges(); + } + private Chapter Map(ChapterInfo chapterInfo, int index, Guid itemId) { return new Chapter() diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs new file mode 100644 index 0000000000..7532e56c60 --- /dev/null +++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Controller.Chapters; + +/// +/// Interface IChapterManager. +/// +public interface IChapterManager +{ + /// + /// Saves the chapters. + /// + /// The video. + /// The set of chapters. + void SaveChapters(Video video, IReadOnlyList chapters); + + /// + /// Gets a single chapter of a BaseItem on a specific index. + /// + /// The BaseItems id. + /// The index of that chapter. + /// A chapter instance. + ChapterInfo? GetChapter(Guid baseItemId, int index); + + /// + /// Gets all chapters associated with the baseItem. + /// + /// The BaseItems id. + /// A readonly list of chapter instances. + IReadOnlyList GetChapters(Guid baseItemId); + + /// + /// Refreshes the chapter images. + /// + /// Video to use. + /// Directory service to use. + /// Set of chapters to refresh. + /// Option to extract images. + /// Option to save chapters. + /// CancellationToken to use for operation. + /// true if successful, false if not. + Task RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken); + + /// + /// Deletes the chapter images. + /// + /// Video to use. + void DeleteChapterImages(Video video); +} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index d1a6b35841..a7ff75bb1c 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -34,7 +34,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities @@ -484,7 +483,7 @@ namespace MediaBrowser.Controller.Entities public static IItemRepository ItemRepository { get; set; } - public static IChapterRepository ChapterRepository { get; set; } + public static IChapterManager ChapterManager { get; set; } public static IFileSystem FileSystem { get; set; } @@ -2051,7 +2050,7 @@ namespace MediaBrowser.Controller.Entities { if (imageType == ImageType.Chapter) { - var chapter = ChapterRepository.GetChapter(this.Id, imageIndex); + var chapter = ChapterManager.GetChapter(Id, imageIndex); if (chapter is null) { @@ -2101,7 +2100,7 @@ namespace MediaBrowser.Controller.Entities if (image.Type == ImageType.Chapter) { - var chapters = ChapterRepository.GetChapters(this.Id); + var chapters = ChapterManager.GetChapters(Id); for (var i = 0; i < chapters.Count; i++) { if (chapters[i].ImagePath == image.Path) diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs index 7c20164a6f..4e4eb514e6 100644 --- a/MediaBrowser.Controller/IO/IPathManager.cs +++ b/MediaBrowser.Controller/IO/IPathManager.cs @@ -1,5 +1,4 @@ using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.IO; @@ -46,4 +45,19 @@ public interface IPathManager /// The media source id. /// The absolute path. public string GetAttachmentFolderPath(string mediaSourceId); + + /// + /// Gets the chapter images data path. + /// + /// The base item. + /// The chapter images data path. + public string GetChapterImageFolderPath(BaseItem item); + + /// + /// Gets the chapter images path. + /// + /// The base item. + /// The chapter position. + /// The chapter images data path. + public string GetChapterImagePath(BaseItem item, long chapterPositionTicks); } diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs deleted file mode 100644 index 8ce40a58d1..0000000000 --- a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs +++ /dev/null @@ -1,28 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.MediaEncoding -{ - public interface IEncodingManager - { - /// - /// Refreshes the chapter images. - /// - /// Video to use. - /// Directory service to use. - /// Set of chapters to refresh. - /// Option to extract images. - /// Option to save chapters. - /// CancellationToken to use for operation. - /// true if successful, false if not. - Task RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken); - } -} diff --git a/MediaBrowser.Controller/Chapters/IChapterRepository.cs b/MediaBrowser.Controller/Persistence/IChapterRepository.cs similarity index 60% rename from MediaBrowser.Controller/Chapters/IChapterRepository.cs rename to MediaBrowser.Controller/Persistence/IChapterRepository.cs index e22cb0f584..0844ddb364 100644 --- a/MediaBrowser.Controller/Chapters/IChapterRepository.cs +++ b/MediaBrowser.Controller/Persistence/IChapterRepository.cs @@ -1,15 +1,20 @@ using System; using System.Collections.Generic; -using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Controller.Chapters; +namespace MediaBrowser.Controller.Persistence; /// -/// Interface IChapterManager. +/// Interface IChapterRepository. /// public interface IChapterRepository { + /// + /// Deletes the chapters. + /// + /// The item. + void DeleteChapters(Guid itemId); + /// /// Saves the chapters. /// @@ -17,21 +22,6 @@ public interface IChapterRepository /// The set of chapters. void SaveChapters(Guid itemId, IReadOnlyList chapters); - /// - /// Gets all chapters associated with the baseItem. - /// - /// The baseitem. - /// A readonly list of chapter instances. - IReadOnlyList GetChapters(BaseItemDto baseItem); - - /// - /// Gets a single chapter of a BaseItem on a specific index. - /// - /// The baseitem. - /// The index of that chapter. - /// A chapter instance. - ChapterInfo? GetChapter(BaseItemDto baseItem, int index); - /// /// Gets all chapters associated with the baseItem. /// diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 0bb21b2878..286ba0de0d 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -32,7 +32,6 @@ namespace MediaBrowser.Providers.MediaInfo private const char InternalValueSeparator = '\u001F'; private readonly IMediaEncoder _mediaEncoder; - private readonly IItemRepository _itemRepo; private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; private readonly IMediaSourceManager _mediaSourceManager; @@ -46,7 +45,6 @@ namespace MediaBrowser.Providers.MediaInfo /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. @@ -55,14 +53,12 @@ namespace MediaBrowser.Providers.MediaInfo ILogger logger, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, - IItemRepository itemRepo, ILibraryManager libraryManager, LyricResolver lyricResolver, ILyricManager lyricManager, IMediaStreamRepository mediaStreamRepository) { _mediaEncoder = mediaEncoder; - _itemRepo = itemRepo; _libraryManager = libraryManager; _logger = logger; _mediaSourceManager = mediaSourceManager; diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 266e1861f9..7947ba9218 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -34,13 +34,11 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ILogger _logger; private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IItemRepository _itemRepo; private readonly IBlurayExaminer _blurayExaminer; private readonly ILocalizationManager _localization; - private readonly IEncodingManager _encodingManager; + private readonly IChapterManager _chapterManager; private readonly IServerConfigurationManager _config; private readonly ISubtitleManager _subtitleManager; - private readonly IChapterRepository _chapterManager; private readonly ILibraryManager _libraryManager; private readonly AudioResolver _audioResolver; private readonly SubtitleResolver _subtitleResolver; @@ -51,13 +49,11 @@ namespace MediaBrowser.Providers.MediaInfo ILogger logger, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, - IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, - IEncodingManager encodingManager, + IChapterManager chapterManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, - IChapterRepository chapterManager, ILibraryManager libraryManager, AudioResolver audioResolver, SubtitleResolver subtitleResolver, @@ -67,13 +63,11 @@ namespace MediaBrowser.Providers.MediaInfo _logger = logger; _mediaSourceManager = mediaSourceManager; _mediaEncoder = mediaEncoder; - _itemRepo = itemRepo; _blurayExaminer = blurayExaminer; _localization = localization; - _encodingManager = encodingManager; + _chapterManager = chapterManager; _config = config; _subtitleManager = subtitleManager; - _chapterManager = chapterManager; _libraryManager = libraryManager; _audioResolver = audioResolver; _subtitleResolver = subtitleResolver; @@ -298,9 +292,9 @@ namespace MediaBrowser.Providers.MediaInfo extractDuringScan = libraryOptions.ExtractChapterImagesDuringLibraryScan; } - await _encodingManager.RefreshChapterImages(video, options.DirectoryService, chapters, extractDuringScan, false, cancellationToken).ConfigureAwait(false); + await _chapterManager.RefreshChapterImages(video, options.DirectoryService, chapters, extractDuringScan, false, cancellationToken).ConfigureAwait(false); - _chapterManager.SaveChapters(video.Id, chapters); + _chapterManager.SaveChapters(video, chapters); } } diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 1c2f8b9134..ba6034ec12 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -55,13 +55,11 @@ namespace MediaBrowser.Providers.MediaInfo /// /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. + /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. /// Instance of the interface. /// Instance of the . /// Instance of the interface. @@ -72,13 +70,11 @@ namespace MediaBrowser.Providers.MediaInfo public ProbeProvider( IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, - IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, - IEncodingManager encodingManager, + IChapterManager chapterManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, - IChapterRepository chapterManager, ILibraryManager libraryManager, IFileSystem fileSystem, ILoggerFactory loggerFactory, @@ -96,13 +92,11 @@ namespace MediaBrowser.Providers.MediaInfo loggerFactory.CreateLogger(), mediaSourceManager, mediaEncoder, - itemRepo, blurayExaminer, localization, - encodingManager, + chapterManager, config, subtitleManager, - chapterManager, libraryManager, _audioResolver, _subtitleResolver, @@ -113,7 +107,6 @@ namespace MediaBrowser.Providers.MediaInfo loggerFactory.CreateLogger(), mediaSourceManager, mediaEncoder, - itemRepo, libraryManager, _lyricResolver, lyricManager, From 8ee358de2ca956d22c14f28c3783ba99acd87a32 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Apr 2025 18:30:25 +0300 Subject: [PATCH 219/508] Check for path overlaps (#12832) --- .../AppBase/BaseApplicationPaths.cs | 45 ++++++++++++++++++- .../AppBase/BaseConfigurationManager.cs | 1 + .../Library/LibraryManager.cs | 2 - .../ServerApplicationPaths.cs | 7 +++ Jellyfin.Server/Program.cs | 1 + .../EncodingConfigurationExtensions.cs | 3 +- .../Configuration/IApplicationPaths.cs | 13 ++++++ .../IServerApplicationPaths.cs | 4 ++ 8 files changed, 71 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index f0cca9efd0..d1376f18ad 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using MediaBrowser.Common.Configuration; namespace Emby.Server.Implementations.AppBase @@ -30,7 +32,6 @@ namespace Emby.Server.Implementations.AppBase ConfigurationDirectoryPath = configurationDirectoryPath; CachePath = cacheDirectoryPath; WebPath = webDirectoryPath; - DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; } @@ -75,5 +76,47 @@ namespace Emby.Server.Implementations.AppBase /// public string TrickplayPath => Path.Combine(DataPath, "trickplay"); + + /// + public virtual void MakeSanityCheckOrThrow() + { + CreateAndCheckMarker(ConfigurationDirectoryPath, "config"); + CreateAndCheckMarker(LogDirectoryPath, "log"); + CreateAndCheckMarker(PluginsPath, "plugin"); + CreateAndCheckMarker(ProgramDataPath, "data"); + CreateAndCheckMarker(CachePath, "cache"); + CreateAndCheckMarker(DataPath, "data"); + } + + /// + public void CreateAndCheckMarker(string path, string markerName, bool recursive = false) + { + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive); + } + + private IEnumerable GetMarkers(string path, bool recursive = false) + { + return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + } + + private void CheckOrCreateMarker(string path, string markerName, bool recursive = false) + { + var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName); + if (otherMarkers != null) + { + throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}."); + } + + var markerPath = Path.Combine(path, markerName); + if (!File.Exists(markerPath)) + { + File.Create(markerPath).Dispose(); + } + } } } diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 9bc3a0204b..81ef0e5f9a 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -227,6 +227,7 @@ namespace Emby.Server.Implementations.AppBase Logger.LogInformation("Setting cache path: {Path}", cachePath); ((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath; + CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache"); } /// diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 40045782b6..a6eddbbc3b 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -768,8 +768,6 @@ namespace Emby.Server.Implementations.Library { var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath; - Directory.CreateDirectory(rootFolderPath); - var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOperationException("Something went very wong")) .DeepCopy(); diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index 725df98da5..f049e66475 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -96,5 +96,12 @@ namespace Emby.Server.Implementations /// public string VirtualInternalMetadataPath => "%MetadataPath%"; + + /// + public override void MakeSanityCheckOrThrow() + { + base.MakeSanityCheckOrThrow(); + CreateAndCheckMarker(RootFolderPath, "root"); + } } } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 8d0bf73f6a..511306755b 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -77,6 +77,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); + appPaths.MakeSanityCheckOrThrow(); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); diff --git a/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs b/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs index 70a4fe4098..78e96ab47c 100644 --- a/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs +++ b/MediaBrowser.Common/Configuration/EncodingConfigurationExtensions.cs @@ -35,8 +35,7 @@ namespace MediaBrowser.Common.Configuration transcodingTempPath = Path.Combine(configurationManager.CommonApplicationPaths.CachePath, "transcodes"); } - // Make sure the directory exists - Directory.CreateDirectory(transcodingTempPath); + configurationManager.CommonApplicationPaths.CreateAndCheckMarker(transcodingTempPath, "transcode", true); return transcodingTempPath; } } diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index 7a8ab32361..fa0d8247b4 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -90,5 +90,18 @@ namespace MediaBrowser.Common.Configuration /// /// The trickplay path. string TrickplayPath { get; } + + /// + /// Checks and creates all known base paths. + /// + void MakeSanityCheckOrThrow(); + + /// + /// Checks and creates the given path and adds it with a marker file if non existant. + /// + /// The path to check. + /// The common marker file name. + /// Check for other settings paths recursivly. + void CreateAndCheckMarker(string path, string markerName, bool recursive = false); } } diff --git a/MediaBrowser.Controller/IServerApplicationPaths.cs b/MediaBrowser.Controller/IServerApplicationPaths.cs index 608286cd8a..a6e83a02c7 100644 --- a/MediaBrowser.Controller/IServerApplicationPaths.cs +++ b/MediaBrowser.Controller/IServerApplicationPaths.cs @@ -2,6 +2,10 @@ #pragma warning disable CS1591 +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using MediaBrowser.Common.Configuration; namespace MediaBrowser.Controller From 2ea7af777b99b52d2291495596defa97408fe6d0 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Apr 2025 18:30:57 +0300 Subject: [PATCH 220/508] Create directory before checking for size (#13962) --- .../StorageHelpers/StorageHelper.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs index 635644179f..e351160c16 100644 --- a/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs +++ b/Jellyfin.Server.Implementations/StorageHelpers/StorageHelper.cs @@ -72,6 +72,11 @@ public static class StorageHelper private static void TestDataDirectorySize(string path, ILogger logger, long threshold = -1) { logger.LogDebug("Check path {TestPath} for storage capacity", path); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + var drive = new DriveInfo(path); if (threshold != -1 && drive.AvailableFreeSpace < threshold) { From 5d65cfcd994d44665cc7804b3a8e9049d4915788 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Apr 2025 18:32:12 +0300 Subject: [PATCH 221/508] improved performance of save operations (#13889) --- .../Item/BaseItemRepository.cs | 143 +++++++++++------- .../Item/PeopleRepository.cs | 55 ++++--- .../Manager/MetadataService.cs | 5 +- 3 files changed, 124 insertions(+), 79 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 7f4364cf61..e7f82389db 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -453,11 +453,9 @@ public sealed class BaseItemRepository var images = item.ImageInfos.Select(e => Map(item.Id, e)); using var context = _dbProvider.CreateDbContext(); - using var transaction = context.Database.BeginTransaction(); context.BaseItemImageInfos.Where(e => e.ItemId == item.Id).ExecuteDelete(); context.BaseItemImageInfos.AddRange(images); context.SaveChanges(); - transaction.Commit(); } /// @@ -487,17 +485,19 @@ public sealed class BaseItemRepository tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags)); } - var localItemValueCache = new Dictionary<(int MagicNumber, string Value), Guid>(); - using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); + + var ids = tuples.Select(f => f.Item.Id).ToArray(); + var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray(); + foreach (var item in tuples) { var entity = Map(item.Item); // TODO: refactor this "inconsistency" entity.TopParentId = item.TopParent?.Id; - if (!context.BaseItems.Any(e => e.Id == entity.Id)) + if (!existingItems.Any(e => e == entity.Id)) { context.BaseItems.Add(entity); } @@ -506,59 +506,98 @@ public sealed class BaseItemRepository context.BaseItemProviders.Where(e => e.ItemId == entity.Id).ExecuteDelete(); context.BaseItems.Attach(entity).State = EntityState.Modified; } + } - context.AncestorIds.Where(e => e.ItemId == entity.Id).ExecuteDelete(); - if (item.Item.SupportsAncestors && item.AncestorIds != null) + context.SaveChanges(); + + var itemValueMaps = tuples + .Select(e => (Item: e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags))) + .ToArray(); + var allListedItemValues = itemValueMaps + .SelectMany(f => f.Values) + .Distinct() + .ToArray(); + var existingValues = context.ItemValues + .Select(e => new { - foreach (var ancestorId in item.AncestorIds) - { - if (!context.BaseItems.Any(f => f.Id == ancestorId)) - { - continue; - } + item = e, + Key = e.Type + "+" + e.Value + }) + .Where(f => allListedItemValues.Select(e => $"{(int)e.MagicNumber}+{e.Value}").Contains(f.Key)) + .Select(e => e.item) + .ToArray(); + var missingItemValues = allListedItemValues.Except(existingValues.Select(f => (MagicNumber: f.Type, f.Value))).Select(f => new ItemValue() + { + CleanValue = GetCleanValue(f.Value), + ItemValueId = Guid.NewGuid(), + Type = f.MagicNumber, + Value = f.Value + }).ToArray(); + context.ItemValues.AddRange(missingItemValues); + context.SaveChanges(); - context.AncestorIds.Add(new AncestorId() + var itemValuesStore = existingValues.Concat(missingItemValues).ToArray(); + var valueMap = itemValueMaps + .Select(f => (Item: f.Item, Values: f.Values.Select(e => itemValuesStore.First(g => g.Value == e.Value && g.Type == e.MagicNumber)).ToArray())) + .ToArray(); + + var mappedValues = context.ItemValuesMap.Where(e => ids.Contains(e.ItemId)).ToList(); + + foreach (var item in valueMap) + { + var itemMappedValues = mappedValues.Where(e => e.ItemId == item.Item.Id).ToList(); + foreach (var itemValue in item.Values) + { + var existingItem = itemMappedValues.FirstOrDefault(f => f.ItemValueId == itemValue.ItemValueId); + if (existingItem is null) + { + context.ItemValuesMap.Add(new ItemValueMap() { - ParentItemId = ancestorId, - ItemId = entity.Id, Item = null!, - ParentItem = null! + ItemId = item.Item.Id, + ItemValue = null!, + ItemValueId = itemValue.ItemValueId }); } + else + { + // map exists, remove from list so its been handled. + itemMappedValues.Remove(existingItem); + } } - // Never save duplicate itemValues as they are now mapped anyway. - var itemValuesToSave = GetItemValuesToSave(item.Item, item.InheritedTags).DistinctBy(e => (GetCleanValue(e.Value), e.MagicNumber)); - context.ItemValuesMap.Where(e => e.ItemId == entity.Id).ExecuteDelete(); - foreach (var itemValue in itemValuesToSave) + // all still listed values are not in the new list so remove them. + context.ItemValuesMap.RemoveRange(itemMappedValues); + } + + context.SaveChanges(); + + foreach (var item in tuples) + { + if (item.Item.SupportsAncestors && item.AncestorIds != null) { - if (!localItemValueCache.TryGetValue(itemValue, out var refValue)) + var existingAncestorIds = context.AncestorIds.Where(e => e.ItemId == item.Item.Id).ToList(); + var validAncestorIds = context.BaseItems.Where(e => item.AncestorIds.Contains(e.Id)).Select(f => f.Id).ToArray(); + foreach (var ancestorId in validAncestorIds) { - refValue = context.ItemValues - .Where(f => f.Value == itemValue.Value && (int)f.Type == itemValue.MagicNumber) - .Select(e => e.ItemValueId) - .FirstOrDefault(); - } - - if (refValue.IsEmpty()) - { - context.ItemValues.Add(new ItemValue() + var existingAncestorId = existingAncestorIds.FirstOrDefault(e => e.ParentItemId == ancestorId); + if (existingAncestorId is null) { - CleanValue = GetCleanValue(itemValue.Value), - Type = (ItemValueType)itemValue.MagicNumber, - ItemValueId = refValue = Guid.NewGuid(), - Value = itemValue.Value - }); - localItemValueCache[itemValue] = refValue; + context.AncestorIds.Add(new AncestorId() + { + ParentItemId = ancestorId, + ItemId = item.Item.Id, + Item = null!, + ParentItem = null! + }); + } + else + { + existingAncestorIds.Remove(existingAncestorId); + } } - context.ItemValuesMap.Add(new ItemValueMap() - { - Item = null!, - ItemId = entity.Id, - ItemValue = null!, - ItemValueId = refValue - }); + context.AncestorIds.RemoveRange(existingAncestorIds); } } @@ -1102,27 +1141,27 @@ public sealed class BaseItemRepository return value.RemoveDiacritics().ToLowerInvariant(); } - private List<(int MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List inheritedTags) + private List<(ItemValueType MagicNumber, string Value)> GetItemValuesToSave(BaseItemDto item, List inheritedTags) { - var list = new List<(int, string)>(); + var list = new List<(ItemValueType, string)>(); if (item is IHasArtist hasArtist) { - list.AddRange(hasArtist.Artists.Select(i => (0, i))); + list.AddRange(hasArtist.Artists.Select(i => ((ItemValueType)0, i))); } if (item is IHasAlbumArtist hasAlbumArtist) { - list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (1, i))); + list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => (ItemValueType.AlbumArtist, i))); } - list.AddRange(item.Genres.Select(i => (2, i))); - list.AddRange(item.Studios.Select(i => (3, i))); - list.AddRange(item.Tags.Select(i => (4, i))); + list.AddRange(item.Genres.Select(i => (ItemValueType.Genre, i))); + list.AddRange(item.Studios.Select(i => (ItemValueType.Studios, i))); + list.AddRange(item.Tags.Select(i => (ItemValueType.Tags, i))); // keywords was 5 - list.AddRange(inheritedTags.Select(i => (6, i))); + list.AddRange(inheritedTags.Select(i => (ItemValueType.InheritedTags, i))); // Remove all invalid values. list.RemoveAll(i => string.IsNullOrWhiteSpace(i.Item2)); diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 77877835e0..4e898119b7 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -5,6 +5,7 @@ using System.Linq; using Jellyfin.Data.Enums; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; +using Jellyfin.Database.Implementations.Entities.Libraries; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; @@ -68,34 +69,41 @@ public class PeopleRepository(IDbContextFactory dbProvider, I public void UpdatePeople(Guid itemId, IReadOnlyList people) { using var context = _dbProvider.CreateDbContext(); - using var transaction = context.Database.BeginTransaction(); - context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ExecuteDelete(); // TODO: yes for __SOME__ reason there can be duplicates. - foreach (var item in people.DistinctBy(e => e.Id)) - { - var personEntity = Map(item); - var existingEntity = context.Peoples.FirstOrDefault(e => e.Id == personEntity.Id); - if (existingEntity is null) - { - context.Peoples.Add(personEntity); - existingEntity = personEntity; - } + people = people.DistinctBy(e => e.Id).ToArray(); + var personids = people.Select(f => f.Id); + var existingPersons = context.Peoples.Where(p => personids.Contains(p.Id)).Select(f => f.Id).ToArray(); + context.Peoples.AddRange(people.Where(e => !existingPersons.Contains(e.Id)).Select(Map)); + context.SaveChanges(); - context.PeopleBaseItemMap.Add(new PeopleBaseItemMap() + var maps = context.PeopleBaseItemMap.Where(e => e.ItemId == itemId).ToList(); + foreach (var person in people) + { + var existingMap = maps.FirstOrDefault(e => e.PeopleId == person.Id); + if (existingMap is null) { - Item = null!, - ItemId = itemId, - People = existingEntity, - PeopleId = existingEntity.Id, - ListOrder = item.SortOrder, - SortOrder = item.SortOrder, - Role = item.Role - }); + context.PeopleBaseItemMap.Add(new PeopleBaseItemMap() + { + Item = null!, + ItemId = itemId, + People = null!, + PeopleId = person.Id, + ListOrder = person.SortOrder, + SortOrder = person.SortOrder, + Role = person.Role + }); + } + else + { + // person mapping already exists so remove from list + maps.Remove(existingMap); + } } + context.PeopleBaseItemMap.RemoveRange(maps); + context.SaveChanges(); - transaction.Commit(); } private PersonInfo Map(People people) @@ -133,9 +141,8 @@ public class PeopleRepository(IDbContextFactory dbProvider, I if (filter.User is not null && filter.IsFavorite.HasValue) { var personType = itemTypeLookup.BaseItemKindNames[BaseItemKind.Person]; - query = query.Where(e => e.PersonType == personType) - .Where(e => context.BaseItems.Where(d => d.UserData!.Any(w => w.IsFavorite == filter.IsFavorite && w.UserId.Equals(filter.User.Id))) - .Select(f => f.Name).Contains(e.Name)); + query = query + .Where(e => context.BaseItems.Any(b => b.Type == personType && b.Name == e.Name && b.UserData!.Any(u => u.IsFavorite == filter.IsFavorite && u.UserId.Equals(filter.User.Id)))); } if (!filter.ItemId.IsEmpty()) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 45f66f85fc..413bf2d30b 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -128,8 +128,7 @@ namespace MediaBrowser.Providers.Manager var metadataResult = new MetadataResult { - Item = itemOfType, - People = LibraryManager.GetPeople(item) + Item = itemOfType }; var beforeSaveResult = BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType); @@ -253,7 +252,7 @@ namespace MediaBrowser.Providers.Manager protected async Task SaveItemAsync(MetadataResult result, ItemUpdateType reason, CancellationToken cancellationToken) { - if (result.Item.SupportsPeople) + if (result.Item.SupportsPeople && result.People is not null) { var baseItem = result.Item; From a0b3b7335fccf028701e94af31028184bfd4e6cb Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 26 Apr 2025 17:35:57 +0200 Subject: [PATCH 222/508] Add .gitignore style ignoring (#13906) --- Directory.Packages.props | 3 +- .../Emby.Server.Implementations.csproj | 4 + .../Library/CoreResolutionIgnoreRule.cs | 4 +- .../Library/DotIgnoreIgnoreRule.cs | 77 +++++++++++++++++++ .../Library/IgnorePatterns.cs | 1 - .../Library/LibraryManager.cs | 24 ++---- .../Library/DotIgnoreIgnoreRuleTest.cs | 30 ++++++++ 7 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs create mode 100644 tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index cdce608d9c..a24b081dd2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,6 +21,7 @@ + @@ -88,4 +89,4 @@ - \ No newline at end of file + diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index d99923b4fc..15843730e9 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -64,6 +64,10 @@ + + + + diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index b01fd93a7b..f29a0b3ad7 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library { if (parent is not null) { - // Ignore extras folders but allow it at the collection level + // Ignore extras for unsupported types if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename) && parent is not AggregateFolder && parent is not UserRootFolder) @@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.Library { if (parent is not null) { - // Don't resolve these into audio files + // Don't resolve theme songs if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal) && AudioFileParser.IsAudioFile(filename, _namingOptions)) { diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs new file mode 100644 index 0000000000..2c186c9173 --- /dev/null +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Model.IO; + +namespace Emby.Server.Implementations.Library; + +/// +/// Resolver rule class for ignoring files via .ignore. +/// +public class DotIgnoreIgnoreRule : IResolverIgnoreRule +{ + private static FileInfo? FindIgnoreFile(DirectoryInfo directory) + { + var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore")); + if (ignoreFile.Exists) + { + return ignoreFile; + } + + var parentDir = directory.Parent; + if (parentDir == null || parentDir.FullName == directory.FullName) + { + return null; + } + + return FindIgnoreFile(parentDir); + } + + /// + public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) + { + return IsIgnored(fileInfo, parent); + } + + /// + /// Checks whether or not the file is ignored. + /// + /// The file information. + /// The parent BaseItem. + /// True if the file should be ignored. + public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent) + { + var parentDirPath = Path.GetDirectoryName(fileInfo.FullName); + if (string.IsNullOrEmpty(parentDirPath)) + { + return false; + } + + var folder = new DirectoryInfo(parentDirPath); + var ignoreFile = FindIgnoreFile(folder); + if (ignoreFile is null) + { + return false; + } + + string ignoreFileString; + using (var reader = ignoreFile.OpenText()) + { + ignoreFileString = reader.ReadToEnd(); + } + + if (string.IsNullOrEmpty(ignoreFileString)) + { + // Ignore directory if we just have the file + return true; + } + + // If file has content, base ignoring off the content .gitignore-style rules + var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var ignore = new Ignore.Ignore(); + ignore.Add(ignoreRules); + + return ignore.IsIgnored(fileInfo.FullName); + } +} diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index bb45dd87e9..25ddade829 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using DotNet.Globbing; namespace Emby.Server.Implementations.Library diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index a6eddbbc3b..21c953fb2d 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -649,10 +649,11 @@ namespace Emby.Server.Implementations.Library args.FileSystemChildren = files; } - // Check to see if we should resolve based on our contents - if (args.IsDirectory && !ShouldResolvePathContents(args)) + // Filter content based on ignore rules + if (args.IsDirectory) { - return null; + var filtered = args.GetActualFileSystemChildren().ToArray(); + args.FileSystemChildren = filtered ?? []; } return ResolveItem(args, resolvers); @@ -683,17 +684,6 @@ namespace Emby.Server.Implementations.Library return newList; } - /// - /// Determines whether a path should be ignored based on its contents - called after the contents have been read. - /// - /// The args. - /// true if XXXX, false otherwise. - private static bool ShouldResolvePathContents(ItemResolveArgs args) - { - // Ignore any folders containing a file called .ignore - return !args.ContainsFileSystemEntryByName(".ignore"); - } - public IEnumerable ResolvePaths(IEnumerable files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, CollectionType? collectionType = null) { return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers); @@ -2724,16 +2714,18 @@ namespace Emby.Server.Implementations.Library public IEnumerable FindExtras(BaseItem owner, IReadOnlyList fileSystemChildren, IDirectoryService directoryService) { + // Apply .ignore rules + var filtered = fileSystemChildren.Where(c => !DotIgnoreIgnoreRule.IsIgnored(c, owner)).ToList(); var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.ContainingFolderPath); if (ownerVideoInfo is null) { yield break; } - var count = fileSystemChildren.Count; + var count = filtered.Count; for (var i = 0; i < count; i++) { - var current = fileSystemChildren[i]; + var current = filtered[i]; if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name)) { var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs new file mode 100644 index 0000000000..d677c9f091 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs @@ -0,0 +1,30 @@ +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Library; + +public class DotIgnoreIgnoreRuleTest +{ + [Fact] + public void Test() + { + var ignore = new Ignore.Ignore(); + ignore.Add("SPs"); + Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv")); + Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv")); + Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv")); + } + + [Fact] + public void TestNegatePattern() + { + var ignore = new Ignore.Ignore(); + ignore.Add("SPs"); + ignore.Add("!thebestshot.mkv"); + Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv")); + Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv")); + Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv")); + Assert.True(!ignore.IsIgnored("f:/cd/sps/thebestshot.mkv")); + Assert.True(!ignore.IsIgnored("cd/sps/thebestshot.mkv")); + Assert.True(!ignore.IsIgnored("/cd/sps/thebestshot.mkv")); + } +} From 9092130350024331a1c3b34cd4f9d3932a1348c7 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 26 Apr 2025 17:36:17 +0200 Subject: [PATCH 223/508] Optimize migrations (#13855) --- .../Routines/MigrateKeyframeData.cs | 60 +++---- .../Migrations/Routines/MoveExtractedFiles.cs | 156 +++++++++--------- .../Migrations/Routines/MoveTrickplayFiles.cs | 31 ++-- MediaBrowser.Common/Plugins/BasePluginOfT.cs | 5 +- .../Attachments/AttachmentExtractor.cs | 18 +- .../IJellyfinDatabaseProvider.cs | 4 +- .../SqliteDatabaseProvider.cs | 7 +- 7 files changed, 128 insertions(+), 153 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs index b8e69db8e7..68d7a7b876 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -12,8 +11,6 @@ using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -24,8 +21,7 @@ namespace Jellyfin.Server.Migrations.Routines; /// public class MigrateKeyframeData : IDatabaseMigrationRoutine { - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; private readonly IDbContextFactory _dbProvider; private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; @@ -33,17 +29,14 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine /// /// Initializes a new instance of the class. /// - /// Instance of the interface. /// The logger. /// Instance of the interface. /// The EFCore db factory. public MigrateKeyframeData( - ILibraryManager libraryManager, - ILogger logger, + ILogger logger, IApplicationPaths appPaths, IDbContextFactory dbProvider) { - _libraryManager = libraryManager; _logger = logger; _appPaths = appPaths; _dbProvider = dbProvider; @@ -63,48 +56,34 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine /// public void Perform() { - const int Limit = 100; - int itemCount = 0, offset = 0, previousCount; + const int Limit = 5000; + int itemCount = 0, offset = 0; var sw = Stopwatch.StartNew(); - var itemsQuery = new InternalItemsQuery - { - MediaTypes = [MediaType.Video], - SourceTypes = [SourceType.Library], - IsVirtualItem = false, - IsFolder = false - }; using var context = _dbProvider.CreateDbContext(); + var baseQuery = context.BaseItems.Where(b => b.MediaType == MediaType.Video.ToString() && !b.IsVirtualItem && !b.IsFolder).OrderBy(e => e.Id); + var records = baseQuery.Count(); + _logger.LogInformation("Checking {Count} items for importable keyframe data.", records); + context.KeyframeData.ExecuteDelete(); using var transaction = context.Database.BeginTransaction(); - List keyframes = []; - do { - var result = _libraryManager.GetItemsResult(itemsQuery); - _logger.LogInformation("Importing keyframes for {Count} items", result.TotalRecordCount); - - var items = result.Items; - previousCount = items.Count; - offset += Limit; - foreach (var item in items) + var results = baseQuery.Skip(offset).Take(Limit).Select(b => new Tuple(b.Id, b.Path)).ToList(); + foreach (var result in results) { - if (TryGetKeyframeData(item, out var data)) + if (TryGetKeyframeData(result.Item1, result.Item2, out var data)) { - keyframes.Add(data); - } - - if (++itemCount % 10_000 == 0) - { - context.KeyframeData.AddRange(keyframes); - keyframes.Clear(); - _logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed); + itemCount++; + context.KeyframeData.Add(data); } } - } while (previousCount == Limit); - context.KeyframeData.AddRange(keyframes); + offset += Limit; + _logger.LogInformation("Checked: {Count} - Imported: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed); + } while (offset < records); + context.SaveChanges(); transaction.Commit(); @@ -116,10 +95,9 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine } } - private bool TryGetKeyframeData(BaseItem item, [NotNullWhen(true)] out KeyframeData? data) + private bool TryGetKeyframeData(Guid id, string? path, [NotNullWhen(true)] out KeyframeData? data) { data = null; - var path = item.Path; if (!string.IsNullOrEmpty(path)) { var cachePath = GetCachePath(KeyframeCachePath, path); @@ -127,7 +105,7 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine { data = new() { - ItemId = item.Id, + ItemId = id, KeyframeTicks = keyframeData.KeyframeTicks.ToList(), TotalDuration = keyframeData.TotalDuration }; diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs index f63c5fd409..c5bbcd6f94 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs @@ -1,6 +1,7 @@ #pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; @@ -8,13 +9,14 @@ using System.Linq; using System.Security.Cryptography; using System.Text; using Jellyfin.Data.Enums; +using Jellyfin.Database.Implementations; +using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; -using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.IO; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Routines; @@ -22,34 +24,34 @@ namespace Jellyfin.Server.Migrations.Routines; /// /// Migration to move extracted files to the new directories. /// -public class MoveExtractedFiles : IDatabaseMigrationRoutine +public class MoveExtractedFiles : IMigrationRoutine { private readonly IApplicationPaths _appPaths; - private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; - private readonly IMediaSourceManager _mediaSourceManager; + private readonly IDbContextFactory _dbProvider; private readonly IPathManager _pathManager; + private readonly IFileSystem _fileSystem; /// /// Initializes a new instance of the class. /// /// Instance of the interface. - /// Instance of the interface. /// The logger. - /// Instance of the interface. + /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. public MoveExtractedFiles( IApplicationPaths appPaths, - ILibraryManager libraryManager, ILogger logger, - IMediaSourceManager mediaSourceManager, - IPathManager pathManager) + IPathManager pathManager, + IFileSystem fileSystem, + IDbContextFactory dbProvider) { _appPaths = appPaths; - _libraryManager = libraryManager; _logger = logger; - _mediaSourceManager = mediaSourceManager; _pathManager = pathManager; + _fileSystem = fileSystem; + _dbProvider = dbProvider; } private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); @@ -68,51 +70,41 @@ public class MoveExtractedFiles : IDatabaseMigrationRoutine /// public void Perform() { - const int Limit = 500; + const int Limit = 5000; int itemCount = 0, offset = 0; var sw = Stopwatch.StartNew(); - var itemsQuery = new InternalItemsQuery - { - MediaTypes = [MediaType.Video], - SourceTypes = [SourceType.Library], - IsVirtualItem = false, - IsFolder = false, - Limit = Limit, - StartIndex = offset, - EnableTotalRecordCount = true, - }; - var records = _libraryManager.GetItemsResult(itemsQuery).TotalRecordCount; + using var context = _dbProvider.CreateDbContext(); + var records = context.BaseItems.Count(b => b.MediaType == MediaType.Video.ToString() && !b.IsVirtualItem && !b.IsFolder); _logger.LogInformation("Checking {Count} items for movable extracted files.", records); // Make sure directories exist Directory.CreateDirectory(SubtitleCachePath); Directory.CreateDirectory(AttachmentCachePath); - - itemsQuery.EnableTotalRecordCount = false; do { - itemsQuery.StartIndex = offset; - var result = _libraryManager.GetItemsResult(itemsQuery); + var results = context.BaseItems + .Include(e => e.MediaStreams!.Where(s => s.StreamType == MediaStreamTypeEntity.Subtitle && !s.IsExternal)) + .Where(b => b.MediaType == MediaType.Video.ToString() && !b.IsVirtualItem && !b.IsFolder) + .OrderBy(e => e.Id) + .Skip(offset) + .Take(Limit) + .Select(b => new Tuple?>(b.Id, b.Path, b.MediaStreams)).ToList(); - var items = result.Items; - foreach (var item in items) + foreach (var result in results) { - if (MoveSubtitleAndAttachmentFiles(item)) + if (MoveSubtitleAndAttachmentFiles(result.Item1, result.Item2, result.Item3, context)) { itemCount++; } } offset += Limit; - if (offset % 5_000 == 0) - { - _logger.LogInformation("Checked extracted files for {Count} items in {Time}.", offset, sw.Elapsed); - } + _logger.LogInformation("Checked: {Count} - Moved: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed); } while (offset < records); - _logger.LogInformation("Checked {Checked} items - Moved files for {Items} items in {Time}.", records, itemCount, sw.Elapsed); + _logger.LogInformation("Moved files for {Count} items in {Time}", itemCount, sw.Elapsed); // Get all subdirectories with 1 character names (those are the legacy directories) var subdirectories = Directory.GetDirectories(SubtitleCachePath, "*", SearchOption.AllDirectories).Where(s => s.Length == SubtitleCachePath.Length + 2).ToList(); @@ -134,52 +126,56 @@ public class MoveExtractedFiles : IDatabaseMigrationRoutine _logger.LogInformation("Cleaned up left over subtitles and attachments."); } - private bool MoveSubtitleAndAttachmentFiles(BaseItem item) + private bool MoveSubtitleAndAttachmentFiles(Guid id, string? path, ICollection? mediaStreams, JellyfinDbContext context) { - var mediaStreams = item.GetMediaStreams().Where(s => s.Type == MediaStreamType.Subtitle && !s.IsExternal); - var itemIdString = item.Id.ToString("N", CultureInfo.InvariantCulture); + var itemIdString = id.ToString("N", CultureInfo.InvariantCulture); var modified = false; - foreach (var mediaStream in mediaStreams) + if (mediaStreams is not null) { - if (mediaStream.Codec is null) + foreach (var mediaStream in mediaStreams) { - continue; - } - - var mediaStreamIndex = mediaStream.Index; - var extension = GetSubtitleExtension(mediaStream.Codec); - var oldSubtitleCachePath = GetOldSubtitleCachePath(item.Path, mediaStream.Index, extension); - if (string.IsNullOrEmpty(oldSubtitleCachePath) || !File.Exists(oldSubtitleCachePath)) - { - continue; - } - - var newSubtitleCachePath = _pathManager.GetSubtitlePath(itemIdString, mediaStreamIndex, extension); - if (File.Exists(newSubtitleCachePath)) - { - File.Delete(oldSubtitleCachePath); - } - else - { - var newDirectory = Path.GetDirectoryName(newSubtitleCachePath); - if (newDirectory is not null) + if (mediaStream.Codec is null) { - Directory.CreateDirectory(newDirectory); - File.Move(oldSubtitleCachePath, newSubtitleCachePath, false); - _logger.LogDebug("Moved subtitle {Index} for {Item} from {Source} to {Destination}", mediaStreamIndex, item.Id, oldSubtitleCachePath, newSubtitleCachePath); + continue; + } - modified = true; + var mediaStreamIndex = mediaStream.StreamIndex; + var extension = GetSubtitleExtension(mediaStream.Codec); + var oldSubtitleCachePath = GetOldSubtitleCachePath(path, mediaStreamIndex, extension); + if (string.IsNullOrEmpty(oldSubtitleCachePath) || !File.Exists(oldSubtitleCachePath)) + { + continue; + } + + var newSubtitleCachePath = _pathManager.GetSubtitlePath(itemIdString, mediaStreamIndex, extension); + if (File.Exists(newSubtitleCachePath)) + { + File.Delete(oldSubtitleCachePath); + } + else + { + var newDirectory = Path.GetDirectoryName(newSubtitleCachePath); + if (newDirectory is not null) + { + Directory.CreateDirectory(newDirectory); + File.Move(oldSubtitleCachePath, newSubtitleCachePath, false); + _logger.LogDebug("Moved subtitle {Index} for {Item} from {Source} to {Destination}", mediaStreamIndex, id, oldSubtitleCachePath, newSubtitleCachePath); + + modified = true; + } } } } - var attachments = _mediaSourceManager.GetMediaAttachments(item.Id).Where(a => !string.Equals(a.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)).ToList(); - var shouldExtractOneByOne = attachments.Any(a => !string.IsNullOrEmpty(a.FileName) - && (a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase))); +#pragma warning disable CA1309 // Use ordinal string comparison + var attachments = context.AttachmentStreamInfos.Where(a => a.ItemId.Equals(id) && !string.Equals(a.Codec, "mjpeg")).ToList(); +#pragma warning restore CA1309 // Use ordinal string comparison + var shouldExtractOneByOne = attachments.Any(a => !string.IsNullOrEmpty(a.Filename) + && (a.Filename.Contains('/', StringComparison.OrdinalIgnoreCase) || a.Filename.Contains('\\', StringComparison.OrdinalIgnoreCase))); foreach (var attachment in attachments) { var attachmentIndex = attachment.Index; - var oldAttachmentPath = GetOldAttachmentDataPath(item.Path, attachmentIndex); + var oldAttachmentPath = GetOldAttachmentDataPath(path, attachmentIndex); if (string.IsNullOrEmpty(oldAttachmentPath) || !File.Exists(oldAttachmentPath)) { oldAttachmentPath = GetOldAttachmentCachePath(itemIdString, attachment, shouldExtractOneByOne); @@ -189,7 +185,7 @@ public class MoveExtractedFiles : IDatabaseMigrationRoutine } } - var newAttachmentPath = _pathManager.GetAttachmentPath(itemIdString, attachment.FileName ?? attachmentIndex.ToString(CultureInfo.InvariantCulture)); + var newAttachmentPath = _pathManager.GetAttachmentPath(itemIdString, attachment.Filename ?? attachmentIndex.ToString(CultureInfo.InvariantCulture)); if (File.Exists(newAttachmentPath)) { File.Delete(oldAttachmentPath); @@ -201,7 +197,7 @@ public class MoveExtractedFiles : IDatabaseMigrationRoutine { Directory.CreateDirectory(newDirectory); File.Move(oldAttachmentPath, newAttachmentPath, false); - _logger.LogDebug("Moved attachment {Index} for {Item} from {Source} to {Destination}", attachmentIndex, item.Id, oldAttachmentPath, newAttachmentPath); + _logger.LogDebug("Moved attachment {Index} for {Item} from {Source} to {Destination}", attachmentIndex, id, oldAttachmentPath, newAttachmentPath); modified = true; } @@ -219,8 +215,7 @@ public class MoveExtractedFiles : IDatabaseMigrationRoutine } string filename; - var protocol = _mediaSourceManager.GetPathProtocol(mediaPath); - if (protocol == MediaProtocol.File) + if (_fileSystem.IsPathFile(mediaPath)) { DateTime? date; try @@ -244,7 +239,7 @@ public class MoveExtractedFiles : IDatabaseMigrationRoutine return Path.Join(_appPaths.DataPath, "attachments", filename[..1], filename); } - private string? GetOldAttachmentCachePath(string mediaSourceId, MediaAttachment attachment, bool shouldExtractOneByOne) + private string? GetOldAttachmentCachePath(string mediaSourceId, AttachmentStreamInfo attachment, bool shouldExtractOneByOne) { var attachmentFolderPath = Path.Join(_appPaths.CachePath, "attachments", mediaSourceId); if (shouldExtractOneByOne) @@ -252,16 +247,21 @@ public class MoveExtractedFiles : IDatabaseMigrationRoutine return Path.Join(attachmentFolderPath, attachment.Index.ToString(CultureInfo.InvariantCulture)); } - if (string.IsNullOrEmpty(attachment.FileName)) + if (string.IsNullOrEmpty(attachment.Filename)) { return null; } - return Path.Join(attachmentFolderPath, attachment.FileName); + return Path.Join(attachmentFolderPath, attachment.Filename); } - private string? GetOldSubtitleCachePath(string path, int streamIndex, string outputSubtitleExtension) + private string? GetOldSubtitleCachePath(string? path, int streamIndex, string outputSubtitleExtension) { + if (path is null) + { + return null; + } + DateTime? date; try { diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs index eeb11e14c1..a278138cee 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Migrations.Routines; /// /// Migration to move trickplay files to the new directory. /// -public class MoveTrickplayFiles : IDatabaseMigrationRoutine +public class MoveTrickplayFiles : IMigrationRoutine { private readonly ITrickplayManager _trickplayManager; private readonly IFileSystem _fileSystem; @@ -29,7 +29,11 @@ public class MoveTrickplayFiles : IDatabaseMigrationRoutine /// Instance of the interface. /// Instance of the interface. /// The logger. - public MoveTrickplayFiles(ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILogger logger) + public MoveTrickplayFiles( + ITrickplayManager trickplayManager, + IFileSystem fileSystem, + ILibraryManager libraryManager, + ILogger logger) { _trickplayManager = trickplayManager; _fileSystem = fileSystem; @@ -49,7 +53,7 @@ public class MoveTrickplayFiles : IDatabaseMigrationRoutine /// public void Perform() { - const int Limit = 100; + const int Limit = 5000; int itemCount = 0, offset = 0, previousCount; var sw = Stopwatch.StartNew(); @@ -64,9 +68,6 @@ public class MoveTrickplayFiles : IDatabaseMigrationRoutine do { var trickplayInfos = _trickplayManager.GetTrickplayItemsAsync(Limit, offset).GetAwaiter().GetResult(); - previousCount = trickplayInfos.Count; - offset += Limit; - trickplayQuery.ItemIds = trickplayInfos.Select(i => i.ItemId).Distinct().ToArray(); var items = _libraryManager.GetItemList(trickplayQuery); foreach (var trickplayInfo in trickplayInfos) @@ -77,24 +78,32 @@ public class MoveTrickplayFiles : IDatabaseMigrationRoutine continue; } - if (++itemCount % 1_000 == 0) - { - _logger.LogInformation("Moved {Count} items in {Time}", itemCount, sw.Elapsed); - } - + var moved = false; var oldPath = GetOldTrickplayDirectory(item, trickplayInfo.Width); var newPath = _trickplayManager.GetTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, trickplayInfo.Width, false); if (_fileSystem.DirectoryExists(oldPath)) { _fileSystem.MoveDirectory(oldPath, newPath); + moved = true; } oldPath = GetNewOldTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, trickplayInfo.Width, false); if (_fileSystem.DirectoryExists(oldPath)) { _fileSystem.MoveDirectory(oldPath, newPath); + moved = true; + } + + if (moved) + { + itemCount++; } } + + offset += Limit; + previousCount = trickplayInfos.Count; + + _logger.LogInformation("Checked: {Checked} - Moved: {Count} - Time: {Time}", itemCount, offset, sw.Elapsed); } while (previousCount == Limit); _logger.LogInformation("Moved {Count} items in {Time}", itemCount, sw.Elapsed); diff --git a/MediaBrowser.Common/Plugins/BasePluginOfT.cs b/MediaBrowser.Common/Plugins/BasePluginOfT.cs index 58992ecd73..30c67fa057 100644 --- a/MediaBrowser.Common/Plugins/BasePluginOfT.cs +++ b/MediaBrowser.Common/Plugins/BasePluginOfT.cs @@ -145,10 +145,7 @@ namespace MediaBrowser.Common.Plugins lock (_configurationSaveLock) { var folder = Path.GetDirectoryName(ConfigurationFilePath); - if (!Directory.Exists(folder)) - { - Directory.CreateDirectory(folder); - } + Directory.CreateDirectory(folder); XmlSerializer.SerializeToFile(config, ConfigurationFilePath); } diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 89291c73bf..1f2bc24037 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -133,19 +133,13 @@ namespace MediaBrowser.MediaEncoding.Attachments var outputFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id); using (await _semaphoreLocks.LockAsync(outputFolder, cancellationToken).ConfigureAwait(false)) { - if (!Directory.Exists(outputFolder)) + Directory.CreateDirectory(outputFolder); + var fileNames = Directory.GetFiles(outputFolder, "*", SearchOption.TopDirectoryOnly).Select(f => Path.GetFileName(f)); + var missingFiles = mediaSource.MediaAttachments.Where(a => !fileNames.Contains(a.FileName) && !string.Equals(a.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)); + if (!missingFiles.Any()) { - Directory.CreateDirectory(outputFolder); - } - else - { - var fileNames = Directory.GetFiles(outputFolder, "*", SearchOption.TopDirectoryOnly).Select(f => Path.GetFileName(f)); - var missingFiles = mediaSource.MediaAttachments.Where(a => !fileNames.Contains(a.FileName) && !string.Equals(a.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)); - if (!missingFiles.Any()) - { - // Skip extraction if all files already exist - return; - } + // Skip extraction if all files already exist + return; } var processArgs = string.Format( diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs index 566b521dd0..34ac7dc836 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs @@ -50,7 +50,7 @@ public interface IJellyfinDatabaseProvider /// /// Runs a full Database backup that can later be restored to. /// - /// A cancelation token. + /// A cancellation token. /// A key to identify the backup. /// May throw an NotImplementException if this operation is not supported for this database. Task MigrationBackupFast(CancellationToken cancellationToken); @@ -59,7 +59,7 @@ public interface IJellyfinDatabaseProvider /// Restores a backup that has been previously created by . /// /// The key to the backup from which the current database should be restored from. - /// A cancelation token. + /// A cancellation token. /// A representing the result of the asynchronous operation. Task RestoreBackupFast(string key, CancellationToken cancellationToken); } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index 927ba63b97..ef1bf1769d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -98,10 +98,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider var key = DateTime.UtcNow.ToString("yyyyMMddhhmmss", CultureInfo.InvariantCulture); var path = Path.Combine(_applicationPaths.DataPath, "jellyfin.db"); var backupFile = Path.Combine(_applicationPaths.DataPath, BackupFolderName); - if (!Directory.Exists(backupFile)) - { - Directory.CreateDirectory(backupFile); - } + Directory.CreateDirectory(backupFile); backupFile = Path.Combine(backupFile, $"{key}_jellyfin.db"); File.Copy(path, backupFile); @@ -118,7 +115,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider if (!File.Exists(backupFile)) { - _logger.LogCritical("Tried to restore a backup that does not exist."); + _logger.LogCritical("Tried to restore a backup that does not exist: {Key}", key); return Task.CompletedTask; } From 2e467f29de1d33eaf7e59b1296c79fa1e8373d45 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sat, 26 Apr 2025 23:36:40 +0800 Subject: [PATCH 224/508] Don't pass through timestamp for image extractor (#13999) --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 39c0bfed40..2c57cf8710 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -756,7 +756,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _threads, vf, isAudio ? string.Empty : GetImageResolutionParameter(), - EncodingHelper.GetVideoSyncOption("0", EncoderVersion).Trim(), // passthrough timestamp + EncodingHelper.GetVideoSyncOption("-1", EncoderVersion).Trim(), // auto decide fps mode tempExtractPath); if (offset.HasValue) From 12642543011c55248b26ebb3a6bc65b566ff5e0b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 26 Apr 2025 09:37:23 -0600 Subject: [PATCH 225/508] Update dependency z440.atl.core to 6.22.0 (#13995) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a24b081dd2..ab9f1d81c2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -81,7 +81,7 @@ - + From 67110b512a39a3192ed010408ac78418477c26a9 Mon Sep 17 00:00:00 2001 From: Rasko Date: Sat, 26 Apr 2025 09:35:03 -0400 Subject: [PATCH 226/508] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)=20Translation:=20Jellyfin/Jellyfin=20Translat?= =?UTF-8?q?e-URL:=20https://translate.jellyfin.org/projects/jellyfin/jelly?= =?UTF-8?q?fin-core/nb=5FNO/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Emby.Server.Implementations/Localization/Core/nb.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index b1b6e96ea1..c00eb467f9 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -1,6 +1,6 @@ { "Albums": "Album", - "AppDeviceValues": "App:{0}, Enhet: {1}", + "AppDeviceValues": "App: {0}, Enhet: {1}", "Application": "Program", "Artists": "Artister", "AuthenticationSucceededWithUserName": "{0} har logget inn", @@ -30,7 +30,7 @@ "ItemAddedWithName": "{0} ble lagt til i biblioteket", "ItemRemovedWithName": "{0} ble fjernet fra biblioteket", "LabelIpAddressValue": "IP-adresse: {0}", - "LabelRunningTimeValue": "Spilletid {0}", + "LabelRunningTimeValue": "Spilletid: {0}", "Latest": "Siste", "MessageApplicationUpdated": "Jellyfin-serveren har blitt oppdatert", "MessageApplicationUpdatedTo": "Jellyfin-serveren ble oppdatert til {0}", From f576783ae11d66ab1c4437dd975eefeea638bb3f Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sun, 27 Apr 2025 02:49:30 +0200 Subject: [PATCH 227/508] Fix distinction queries (#14007) --- .../IO/FileRefresher.cs | 2 +- .../IO/LibraryMonitor.cs | 2 +- .../Library/LibraryManager.cs | 6 ++-- .../Controllers/ItemUpdateController.cs | 8 ++--- Jellyfin.Api/Controllers/YearsController.cs | 2 +- MediaBrowser.Controller/Entities/BaseItem.cs | 2 +- .../Entities/Movies/BoxSet.cs | 2 +- .../Encoder/EncoderValidator.cs | 30 ++++++++-------- .../Manager/MetadataService.cs | 8 ++--- .../Music/AlbumMetadataService.cs | 2 +- .../Music/AudioMetadataService.cs | 2 +- .../Music/MusicVideoMetadataService.cs | 3 +- .../Playlists/PlaylistItemsProvider.cs | 2 +- src/Jellyfin.LiveTv/LiveTvManager.cs | 34 +++++++++---------- .../Recordings/RecordingsManager.cs | 2 +- 15 files changed, 54 insertions(+), 53 deletions(-) diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 7378cf8851..f634084034 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.IO private void ProcessPathChanges(List paths) { IEnumerable itemsToRefresh = paths - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .Select(GetAffectedBaseItem) .Where(item => item is not null) .DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where() diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 6af2a553d6..d87ad729ee 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.IO .Where(IsLibraryMonitorEnabled) .OfType() .SelectMany(f => f.PhysicalLocations) - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .Order(); foreach (var path in paths) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 21c953fb2d..64a96c4e5a 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -668,10 +668,10 @@ namespace Emby.Server.Implementations.Library var list = originalList.Where(i => i.IsDirectory) .Select(i => Path.TrimEndingDirectorySeparator(i.FullName)) - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .ToList(); - var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.OrdinalIgnoreCase) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath))) + var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.Ordinal) && list.Any(i => _fileSystem.ContainsSubPath(i, subPath))) .ToList(); foreach (var dupe in dupes) @@ -679,7 +679,7 @@ namespace Emby.Server.Implementations.Library _logger.LogInformation("Found duplicate path: {0}", dupe); } - var newList = list.Except(dupes, StringComparer.OrdinalIgnoreCase).Select(_fileSystem.GetDirectoryInfo).ToList(); + var newList = list.Except(dupes, StringComparer.Ordinal).Select(_fileSystem.GetDirectoryInfo).ToList(); newList.AddRange(originalList.Where(i => !i.IsDirectory)); return newList; } diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index d49e0753ee..50eeaeac67 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -299,7 +299,7 @@ public class ItemUpdateController : BaseJellyfinApiController if (!season.LockedFields.Contains(MetadataField.Tags)) { - season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); + season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } season.OnMetadataChanged(); @@ -316,7 +316,7 @@ public class ItemUpdateController : BaseJellyfinApiController if (!ep.LockedFields.Contains(MetadataField.Tags)) { - ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); + ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } ep.OnMetadataChanged(); @@ -337,7 +337,7 @@ public class ItemUpdateController : BaseJellyfinApiController if (!ep.LockedFields.Contains(MetadataField.Tags)) { - ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); + ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } ep.OnMetadataChanged(); @@ -357,7 +357,7 @@ public class ItemUpdateController : BaseJellyfinApiController if (!track.LockedFields.Contains(MetadataField.Tags)) { - track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray(); + track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } track.OnMetadataChanged(); diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index ebf98da456..b602585863 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -223,6 +223,6 @@ public class YearsController : BaseJellyfinApiController .Select(i => i.ProductionYear ?? 0) .Where(i => i > 0) .Distinct() - .Select(year => _libraryManager.GetYear(year)); + .Select(_libraryManager.GetYear); } } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index a7ff75bb1c..b90ec8222d 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1804,7 +1804,7 @@ namespace MediaBrowser.Controller.Entities public void SetStudios(IEnumerable names) { - Studios = names.Trimmed().Distinct().ToArray(); + Studios = names.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } /// diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index d656fccb4f..dd5852823e 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -197,7 +197,7 @@ namespace MediaBrowser.Controller.Entities.Movies var expandedFolders = new List(); return FlattenItems(this, expandedFolders) - .SelectMany(i => LibraryManager.GetCollectionFolders(i)) + .SelectMany(LibraryManager.GetCollectionFolders) .Select(i => i.Id) .Distinct() .ToArray(); diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index d28cd70ef5..5683de169f 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -14,8 +14,8 @@ namespace MediaBrowser.MediaEncoding.Encoder { public partial class EncoderValidator { - private static readonly string[] _requiredDecoders = new[] - { + private static readonly string[] _requiredDecoders = + [ "h264", "hevc", "vp8", @@ -57,10 +57,10 @@ namespace MediaBrowser.MediaEncoding.Encoder "vp8_rkmpp", "vp9_rkmpp", "av1_rkmpp" - }; + ]; - private static readonly string[] _requiredEncoders = new[] - { + private static readonly string[] _requiredEncoders = + [ "libx264", "libx265", "libsvtav1", @@ -97,10 +97,10 @@ namespace MediaBrowser.MediaEncoding.Encoder "h264_rkmpp", "hevc_rkmpp", "mjpeg_rkmpp" - }; + ]; - private static readonly string[] _requiredFilters = new[] - { + private static readonly string[] _requiredFilters = + [ // sw "alphasrc", "zscale", @@ -148,7 +148,7 @@ namespace MediaBrowser.MediaEncoding.Encoder "scale_rkrga", "vpp_rkrga", "overlay_rkrga" - }; + ]; private static readonly Dictionary _filterOptionsDict = new Dictionary { @@ -471,10 +471,10 @@ namespace MediaBrowser.MediaEncoding.Encoder if (string.IsNullOrWhiteSpace(output)) { - return Enumerable.Empty(); + return []; } - var found = output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct().ToList(); + var found = output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).Skip(1).Distinct().ToList(); _logger.LogInformation("Available hwaccel types: {Types}", found); return found; @@ -580,12 +580,12 @@ namespace MediaBrowser.MediaEncoding.Encoder catch (Exception ex) { _logger.LogError(ex, "Error detecting available {Codec}", codecstr); - return Enumerable.Empty(); + return []; } if (string.IsNullOrWhiteSpace(output)) { - return Enumerable.Empty(); + return []; } var required = codec == Codec.Encoder ? _requiredEncoders : _requiredDecoders; @@ -610,12 +610,12 @@ namespace MediaBrowser.MediaEncoding.Encoder catch (Exception ex) { _logger.LogError(ex, "Error detecting available filters"); - return Enumerable.Empty(); + return []; } if (string.IsNullOrWhiteSpace(output)) { - return Enumerable.Empty(); + return []; } var found = FilterRegex() diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 413bf2d30b..50bbf0974a 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -1041,7 +1041,7 @@ namespace MediaBrowser.Providers.Manager } else { - target.Studios = target.Studios.Concat(source.Studios).Distinct().ToArray(); + target.Studios = target.Studios.Concat(source.Studios).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } } @@ -1053,7 +1053,7 @@ namespace MediaBrowser.Providers.Manager } else { - target.Tags = target.Tags.Concat(source.Tags).Distinct().ToArray(); + target.Tags = target.Tags.Concat(source.Tags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } } @@ -1065,7 +1065,7 @@ namespace MediaBrowser.Providers.Manager } else { - target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray(); + target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } } @@ -1214,7 +1214,7 @@ namespace MediaBrowser.Providers.Manager } else if (sourceHasAlbumArtist.AlbumArtists.Count > 0) { - targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.AlbumArtists).Distinct().ToArray(); + targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.AlbumArtists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } } } diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 0bcc301cb9..64b6273670 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -227,7 +227,7 @@ namespace MediaBrowser.Providers.Music } else { - targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray(); + targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist))) diff --git a/MediaBrowser.Providers/Music/AudioMetadataService.cs b/MediaBrowser.Providers/Music/AudioMetadataService.cs index 7b25bc0e49..71962d952b 100644 --- a/MediaBrowser.Providers/Music/AudioMetadataService.cs +++ b/MediaBrowser.Providers/Music/AudioMetadataService.cs @@ -63,7 +63,7 @@ namespace MediaBrowser.Providers.Music } else { - targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray(); + targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } if (replaceData || string.IsNullOrEmpty(targetItem.Album)) diff --git a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs index 24c4b55015..4022bedc1a 100644 --- a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs +++ b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using System.Linq; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -48,7 +49,7 @@ namespace MediaBrowser.Providers.Music } else { - targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray(); + targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } } } diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs index 51a3ba0c7f..a986b0b699 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs @@ -99,7 +99,7 @@ public class PlaylistItemsProvider : ILocalMetadataProvider, .OfType() .Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Value)) .SelectMany(f => f.PhysicalLocations) - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .ToList(); using (var stream = File.OpenRead(path)) diff --git a/src/Jellyfin.LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs index 40adb51a58..53bc6751fc 100644 --- a/src/Jellyfin.LiveTv/LiveTvManager.cs +++ b/src/Jellyfin.LiveTv/LiveTvManager.cs @@ -125,8 +125,8 @@ namespace Jellyfin.LiveTv IsKids = query.IsKids, IsSports = query.IsSports, IsSeries = query.IsSeries, - IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel }, - TopParentIds = new[] { topFolder.Id }, + IncludeItemTypes = [BaseItemKind.LiveTvChannel], + TopParentIds = [topFolder.Id], IsFavorite = query.IsFavorite, IsLiked = query.IsLiked, StartIndex = query.StartIndex, @@ -199,17 +199,17 @@ namespace Jellyfin.LiveTv if (query.OrderBy.Count == 0) { // Unless something else was specified, order by start date to take advantage of a specialized index - query.OrderBy = new[] - { + query.OrderBy = + [ (ItemSortBy.StartDate, SortOrder.Ascending) - }; + ]; } RemoveFields(options); var internalQuery = new InternalItemsQuery(user) { - IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, + IncludeItemTypes = [BaseItemKind.LiveTvProgram], MinEndDate = query.MinEndDate, MinStartDate = query.MinStartDate, MaxEndDate = query.MaxEndDate, @@ -226,7 +226,7 @@ namespace Jellyfin.LiveTv Limit = query.Limit, OrderBy = query.OrderBy, EnableTotalRecordCount = query.EnableTotalRecordCount, - TopParentIds = new[] { topFolder.Id }, + TopParentIds = [topFolder.Id], Name = query.Name, DtoOptions = options, HasAired = query.HasAired, @@ -272,7 +272,7 @@ namespace Jellyfin.LiveTv var internalQuery = new InternalItemsQuery(user) { - IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, + IncludeItemTypes = [BaseItemKind.LiveTvProgram], IsAiring = query.IsAiring, HasAired = query.HasAired, IsNews = query.IsNews, @@ -281,8 +281,8 @@ namespace Jellyfin.LiveTv IsSports = query.IsSports, IsKids = query.IsKids, EnableTotalRecordCount = query.EnableTotalRecordCount, - OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) }, - TopParentIds = new[] { topFolder.Id }, + OrderBy = [(ItemSortBy.StartDate, SortOrder.Ascending)], + TopParentIds = [topFolder.Id], DtoOptions = options, GenreIds = query.GenreIds }; @@ -497,19 +497,19 @@ namespace Jellyfin.LiveTv // TotalRecordCount = items.Length // }; - dtoOptions.Fields = dtoOptions.Fields.Concat(new[] { ItemFields.Tags }).Distinct().ToArray(); + dtoOptions.Fields = dtoOptions.Fields.Concat([ItemFields.Tags]).Distinct().ToArray(); } var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { - MediaTypes = new[] { MediaType.Video }, + MediaTypes = [MediaType.Video], Recursive = true, AncestorIds = folderIds, IsFolder = false, IsVirtualItem = false, Limit = limit, StartIndex = query.StartIndex, - OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) }, + OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)], EnableTotalRecordCount = query.EnableTotalRecordCount, IncludeItemTypes = includeItemTypes.ToArray(), ExcludeItemTypes = excludeItemTypes.ToArray(), @@ -959,13 +959,13 @@ namespace Jellyfin.LiveTv var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, + IncludeItemTypes = [BaseItemKind.LiveTvProgram], ChannelIds = channelIds, MaxStartDate = now, MinEndDate = now, Limit = channelIds.Length, - OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) }, - TopParentIds = new[] { GetInternalLiveTvFolder(CancellationToken.None).Id }, + OrderBy = [(ItemSortBy.StartDate, SortOrder.Ascending)], + TopParentIds = [GetInternalLiveTvFolder(CancellationToken.None).Id], DtoOptions = options }) : new List(); @@ -1269,7 +1269,7 @@ namespace Jellyfin.LiveTv { var folders = _recordingsManager.GetRecordingFolders() .SelectMany(i => i.Locations) - .Distinct(StringComparer.OrdinalIgnoreCase) + .Distinct() .Select(i => _libraryManager.FindByPath(i, true)) .Where(i => i is not null && i.IsVisibleStandalone(user)) .SelectMany(i => _libraryManager.GetCollectionFolders(i)) diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs index 9ca5d7420b..846f9baf71 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs @@ -230,7 +230,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable if (pathsAdded.Count > 0 || pathsToRemove.Count > 0) { pathsAdded.InsertRange(0, config.MediaLocationsCreated); - config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct().ToArray(); _config.SaveConfiguration("livetv", config); } From 1c4b5199b8fa42dd41d6d779db98650a460c7117 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sun, 27 Apr 2025 04:10:54 +0300 Subject: [PATCH 228/508] Fix ItemValue query (#13939) --- .../Item/BaseItemRepository.cs | 260 +++++++++++------- .../JellyfinQueryHelperExtensions.cs | 166 +++++++++++ 2 files changed, 324 insertions(+), 102 deletions(-) create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index e7f82389db..7d30ac1a07 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -148,37 +148,37 @@ public sealed class BaseItemRepository } /// - public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery filter) + public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAllArtists(InternalItemsQuery filter) { return GetItemValues(filter, _getAllArtistsValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]); } /// - public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery filter) + public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetArtists(InternalItemsQuery filter) { return GetItemValues(filter, _getArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]); } /// - public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery filter) + public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetAlbumArtists(InternalItemsQuery filter) { return GetItemValues(filter, _getAlbumArtistValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]); } /// - public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery filter) + public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetStudios(InternalItemsQuery filter) { return GetItemValues(filter, _getStudiosValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Studio]); } /// - public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery filter) + public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetGenres(InternalItemsQuery filter) { return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.Genre]); } /// - public QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery filter) + public QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetMusicGenres(InternalItemsQuery filter) { return GetItemValues(filter, _getGenreValueTypes, _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicGenre]); } @@ -402,7 +402,8 @@ public sealed class BaseItemRepository private IQueryable PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter) { - IQueryable dbQuery = context.BaseItems.AsNoTracking().AsSplitQuery() + IQueryable dbQuery = context.BaseItems.AsNoTracking(); + dbQuery = dbQuery.AsSingleQuery() .Include(e => e.TrailerTypes) .Include(e => e.Provider) .Include(e => e.LockedFields); @@ -1037,7 +1038,7 @@ public sealed class BaseItemRepository return Map(baseItemEntity, dto, appHost); } - private QueryResult<(BaseItemDto Item, ItemCounts ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyList itemValueTypes, string returnType) + private QueryResult<(BaseItemDto Item, ItemCounts? ItemCounts)> GetItemValues(InternalItemsQuery filter, IReadOnlyList itemValueTypes, string returnType) { ArgumentNullException.ThrowIfNull(filter); @@ -1048,20 +1049,59 @@ public sealed class BaseItemRepository using var context = _dbProvider.CreateDbContext(); - var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter); - - query = query.Where(e => e.Type == returnType); - // this does not seem to be nesseary but it does not make any sense why this isn't working. - // && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type))); - - if (filter.OrderBy.Count != 0 - || !string.IsNullOrEmpty(filter.SearchTerm)) + var innerQueryFilter = TranslateQuery(context.BaseItems, context, new InternalItemsQuery(filter.User) { - query = ApplyOrder(query, filter); - } - else + ExcludeItemTypes = filter.ExcludeItemTypes, + IncludeItemTypes = filter.IncludeItemTypes, + MediaTypes = filter.MediaTypes, + AncestorIds = filter.AncestorIds, + ItemIds = filter.ItemIds, + TopParentIds = filter.TopParentIds, + ParentId = filter.ParentId, + IsAiring = filter.IsAiring, + IsMovie = filter.IsMovie, + IsSports = filter.IsSports, + IsKids = filter.IsKids, + IsNews = filter.IsNews, + IsSeries = filter.IsSeries + }); + + var innerQuery = PrepareItemQuery(context, filter) + .Where(e => e.Type == returnType) + .Where(e => context.ItemValues! + .Where(f => itemValueTypes.Contains(f.Type)) + .Where(f => innerQueryFilter.Any(g => f.BaseItemsMap!.Any(w => w.ItemId == g.Id))) + .Select(f => f.CleanValue) + .Contains(e.CleanName)); + + var outerQueryFilter = new InternalItemsQuery(filter.User) { - query = query.OrderBy(e => e.SortName); + IsPlayed = filter.IsPlayed, + IsFavorite = filter.IsFavorite, + IsFavoriteOrLiked = filter.IsFavoriteOrLiked, + IsLiked = filter.IsLiked, + IsLocked = filter.IsLocked, + NameLessThan = filter.NameLessThan, + NameStartsWith = filter.NameStartsWith, + NameStartsWithOrGreater = filter.NameStartsWithOrGreater, + Tags = filter.Tags, + OfficialRatings = filter.OfficialRatings, + StudioIds = filter.StudioIds, + GenreIds = filter.GenreIds, + Genres = filter.Genres, + Years = filter.Years, + NameContains = filter.NameContains, + SearchTerm = filter.SearchTerm, + ExcludeItemIds = filter.ExcludeItemIds + }; + + var query = TranslateQuery(innerQuery, context, outerQueryFilter) + .GroupBy(e => e.PresentationUniqueKey); + + var result = new QueryResult<(BaseItemDto, ItemCounts?)>(); + if (filter.EnableTotalRecordCount) + { + result.TotalRecordCount = query.Count(); } if (filter.Limit.HasValue || filter.StartIndex.HasValue) @@ -1079,41 +1119,84 @@ public sealed class BaseItemRepository } } - var result = new QueryResult<(BaseItemDto, ItemCounts)>(); - if (filter.EnableTotalRecordCount) - { - result.TotalRecordCount = query.GroupBy(e => e.PresentationUniqueKey).Select(e => e.First()).Count(); - } + IQueryable? itemCountQuery = null; - var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series]; - var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; - var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode]; - var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]; - var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]; - var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; - var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer]; - - var resultQuery = query.Select(e => new + if (filter.IncludeItemTypes.Length > 0) { - item = e, - // TODO: This is bad refactor! - itemCount = new ItemCounts() + // if we are to include more then one type, sub query those items beforehand. + + var typeSubQuery = new InternalItemsQuery(filter.User) { - SeriesCount = e.ItemValues!.Count(f => f.Item.Type == seriesTypeName), - EpisodeCount = e.ItemValues!.Count(f => f.Item.Type == episodeTypeName), - MovieCount = e.ItemValues!.Count(f => f.Item.Type == movieTypeName), - AlbumCount = e.ItemValues!.Count(f => f.Item.Type == musicAlbumTypeName), - ArtistCount = e.ItemValues!.Count(f => f.Item.Type == musicArtistTypeName), - SongCount = e.ItemValues!.Count(f => f.Item.Type == audioTypeName), - TrailerCount = e.ItemValues!.Count(f => f.Item.Type == trailerTypeName), - } - }); + ExcludeItemTypes = filter.ExcludeItemTypes, + IncludeItemTypes = filter.IncludeItemTypes, + MediaTypes = filter.MediaTypes, + AncestorIds = filter.AncestorIds, + ExcludeItemIds = filter.ExcludeItemIds, + ItemIds = filter.ItemIds, + TopParentIds = filter.TopParentIds, + ParentId = filter.ParentId, + IsPlayed = filter.IsPlayed + }; - result.StartIndex = filter.StartIndex ?? 0; - result.Items = resultQuery.ToArray().Where(e => e is not null).Select(e => + itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, typeSubQuery) + .Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type))); + + var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series]; + var movieTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Movie]; + var episodeTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode]; + var musicAlbumTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicAlbum]; + var musicArtistTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.MusicArtist]; + var audioTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Audio]; + var trailerTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Trailer]; + + var resultQuery = query.Select(e => new + { + item = e.AsQueryable() + .Include(e => e.TrailerTypes) + .Include(e => e.Provider) + .Include(e => e.LockedFields) + .Include(e => e.Images) + .AsSingleQuery().First(), + // TODO: This is bad refactor! + itemCount = new ItemCounts() + { + SeriesCount = itemCountQuery!.Count(f => f.Type == seriesTypeName), + EpisodeCount = itemCountQuery!.Count(f => f.Type == episodeTypeName), + MovieCount = itemCountQuery!.Count(f => f.Type == movieTypeName), + AlbumCount = itemCountQuery!.Count(f => f.Type == musicAlbumTypeName), + ArtistCount = itemCountQuery!.Count(f => f.Type == musicArtistTypeName), + SongCount = itemCountQuery!.Count(f => f.Type == audioTypeName), + TrailerCount = itemCountQuery!.Count(f => f.Type == trailerTypeName), + } + }); + + result.StartIndex = filter.StartIndex ?? 0; + result.Items = + [ + .. resultQuery + .AsEnumerable() + .Where(e => e is not null) + .Select(e => + { + return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount); + }) + ]; + } + else { - return (DeserialiseBaseItem(e.item, filter.SkipDeserialization), e.itemCount); - }).ToArray(); + result.StartIndex = filter.StartIndex ?? 0; + result.Items = + [ + .. query + .Select(e => e.First()) + .AsEnumerable() + .Where(e => e is not null) + .Select(e => + { + return (DeserialiseBaseItem(e, filter.SkipDeserialization), null); + }) + ]; + } return result; } @@ -1296,7 +1379,7 @@ public sealed class BaseItemRepository } else if (orderBy.Count == 0) { - return query; + return query.OrderBy(e => e.SortName); } IOrderedQueryable? orderedQuery = null; @@ -1478,6 +1561,7 @@ public sealed class BaseItemRepository } var includeTypes = filter.IncludeItemTypes; + // Only specify excluded types if no included types are specified if (filter.IncludeItemTypes.Length == 0) { @@ -1503,25 +1587,10 @@ public sealed class BaseItemRepository baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type)); } } - else if (includeTypes.Length == 1) + else { - if (_itemTypeLookup.BaseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName)) - { - baseQuery = baseQuery.Where(e => e.Type == includeTypeName); - } - } - else if (includeTypes.Length > 1) - { - var includeTypeName = new List(); - foreach (var includeType in includeTypes) - { - if (_itemTypeLookup.BaseItemKindNames.TryGetValue(includeType, out var baseItemKindName)) - { - includeTypeName.Add(baseItemKindName!); - } - } - - baseQuery = baseQuery.Where(e => includeTypeName.Contains(e.Type)); + string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f)).Where(e => e != null).ToArray()!; + baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type); } if (filter.ChannelIds.Count > 0) @@ -1779,64 +1848,59 @@ public sealed class BaseItemRepository if (filter.ArtistIds.Length > 0) { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type <= ItemValueType.Artist && filter.ArtistIds.Contains(f.ItemId))); + baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ArtistIds); } if (filter.AlbumArtistIds.Length > 0) { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.AlbumArtistIds.Contains(f.ItemId))); + baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.AlbumArtistIds); } if (filter.ContributingArtistIds.Length > 0) { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ContributingArtistIds.Contains(f.ItemId))); + baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ContributingArtistIds); } if (filter.AlbumIds.Length > 0) { - baseQuery = baseQuery.Where(e => context.BaseItems.Where(f => filter.AlbumIds.Contains(f.Id)).Any(f => f.Name == e.Album)); + var subQuery = context.BaseItems.WhereOneOrMany(filter.AlbumIds, f => f.Id); + baseQuery = baseQuery.Where(e => subQuery.Any(f => f.Name == e.Album)); } if (filter.ExcludeArtistIds.Length > 0) { - baseQuery = baseQuery - .Where(e => !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Artist && filter.ExcludeArtistIds.Contains(f.ItemId))); + baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Artist, filter.ExcludeArtistIds, true); } if (filter.GenreIds.Count > 0) { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Genre && filter.GenreIds.Contains(f.ItemId))); + baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Genre, filter.GenreIds.ToArray()); } if (filter.Genres.Count > 0) { - var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray(); + var cleanGenres = filter.Genres.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder(f => f.ItemValue.CleanValue); baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Genre && cleanGenres.Contains(f.ItemValue.CleanValue))); + .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Genre).Any(cleanGenres)); } if (tags.Count > 0) { - var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray(); + var cleanValues = tags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder(f => f.ItemValue.CleanValue); baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && cleanValues.Contains(f.ItemValue.CleanValue))); + .Where(e => e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(cleanValues)); } if (excludeTags.Count > 0) { - var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray(); + var cleanValues = excludeTags.Select(e => GetCleanValue(e)).ToArray().OneOrManyExpressionBuilder(f => f.ItemValue.CleanValue); baseQuery = baseQuery - .Where(e => !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && cleanValues.Contains(f.ItemValue.CleanValue))); + .Where(e => !e.ItemValues!.AsQueryable().Where(f => f.ItemValue.Type == ItemValueType.Tags).Any(cleanValues)); } if (filter.StudioIds.Length > 0) { - baseQuery = baseQuery - .Where(e => e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Studios && filter.StudioIds.Contains(f.ItemId))); + baseQuery = baseQuery.WhereReferencedItem(context, ItemValueType.Studios, filter.StudioIds.ToArray()); } if (filter.OfficialRatings.Length > 0) @@ -2027,15 +2091,9 @@ public sealed class BaseItemRepository .Where(e => !context.Peoples.Any(f => f.Name == e.Name)); } - if (filter.Years.Length == 1) + if (filter.Years.Length > 0) { - baseQuery = baseQuery - .Where(e => e.ProductionYear == filter.Years[0]); - } - else if (filter.Years.Length > 1) - { - baseQuery = baseQuery - .Where(e => filter.Years.Any(f => f == e.ProductionYear)); + baseQuery = baseQuery.WhereOneOrMany(filter.Years, e => e.ProductionYear!.Value); } var isVirtualItem = filter.IsVirtualItem ?? filter.IsMissing; @@ -2076,14 +2134,12 @@ public sealed class BaseItemRepository if (filter.MediaTypes.Length > 0) { var mediaTypes = filter.MediaTypes.Select(f => f.ToString()).ToArray(); - baseQuery = baseQuery - .Where(e => mediaTypes.Contains(e.MediaType)); + baseQuery = baseQuery.WhereOneOrMany(mediaTypes, e => e.MediaType); } if (filter.ItemIds.Length > 0) { - baseQuery = baseQuery - .Where(e => filter.ItemIds.Contains(e.Id)); + baseQuery = baseQuery.WhereOneOrMany(filter.ItemIds, e => e.Id); } if (filter.ExcludeItemIds.Length > 0) @@ -2129,13 +2185,13 @@ public sealed class BaseItemRepository } else { - baseQuery = baseQuery.Where(e => queryTopParentIds.Contains(e.TopParentId!.Value)); + baseQuery = baseQuery.WhereOneOrMany(queryTopParentIds, e => e.TopParentId!.Value); } } if (filter.AncestorIds.Length > 0) { - baseQuery = baseQuery.Where(e => e.Children!.Any(f => filter.AncestorIds.Contains(f.ParentItemId))); + baseQuery = baseQuery.Where(e => e.Children!.Any(f => filter.AncestorIds.Contains(f.ItemId))); } if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey)) diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs new file mode 100644 index 0000000000..4d5cfb8c9b --- /dev/null +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs @@ -0,0 +1,166 @@ +#pragma warning disable RS0030 // Do not use banned APIs + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Jellyfin.Database.Implementations.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Jellyfin.Database.Implementations; + +/// +/// Contains a number of query related extensions. +/// +public static class JellyfinQueryHelperExtensions +{ + private static readonly MethodInfo _containsMethodGenericCache = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static).First(m => m.Name == nameof(Enumerable.Contains) && m.GetParameters().Length == 2); + private static readonly MethodInfo _efParameterInstruction = typeof(EF).GetMethod(nameof(EF.Parameter), BindingFlags.Public | BindingFlags.Static)!; + private static readonly ConcurrentDictionary _containsQueryCache = new(); + + /// + /// Builds an optimised query checking one property against a list of values while maintaining an optimal query. + /// + /// The entity. + /// The property type to compare. + /// The source query. + /// The list of items to check. + /// Property expression. + /// A Query. + public static IQueryable WhereOneOrMany(this IQueryable query, IList oneOf, Expression> property) + { + return query.Where(OneOrManyExpressionBuilder(oneOf, property)); + } + + /// + /// Builds a query that checks referenced ItemValues for a cross BaseItem lookup. + /// + /// The source query. + /// The database context. + /// The type of item value to reference. + /// The list of BaseItem ids to check matches. + /// If set an exclusion check is performed instead. + /// A Query. + public static IQueryable WhereReferencedItem( + this IQueryable baseQuery, + JellyfinDbContext context, + ItemValueType itemValueType, + IList referenceIds, + bool invert = false) + { + return baseQuery.Where(ReferencedItemFilterExpressionBuilder(context, itemValueType, referenceIds, invert)); + } + + /// + /// Builds a query expression that checks referenced ItemValues for a cross BaseItem lookup. + /// + /// The database context. + /// The type of item value to reference. + /// The list of BaseItem ids to check matches. + /// If set an exclusion check is performed instead. + /// A Query. + public static Expression> ReferencedItemFilterExpressionBuilder( + this JellyfinDbContext context, + ItemValueType itemValueType, + IList referenceIds, + bool invert = false) + { + // Well genre/artist/album etc items do not actually set the ItemValue of thier specitic types so we cannot match it that way. + /* + "(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreIds and Type=2)))" + */ + + var itemFilter = OneOrManyExpressionBuilder(referenceIds, f => f.Id); + + return item => + context.ItemValues + .Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (item, map) => new { item, map }) + .Any(val => + val.item.Type == itemValueType + && context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.item.CleanValue) + && val.map.ItemId == item.Id) == EF.Constant(!invert); + } + + /// + /// Builds an optimised query expression checking one property against a list of values while maintaining an optimal query. + /// + /// The entity. + /// The property type to compare. + /// The list of items to check. + /// Property expression. + /// A Query. + public static Expression> OneOrManyExpressionBuilder(this IList oneOf, Expression> property) + { + var parameter = Expression.Parameter(typeof(TEntity), "item"); + property = ParameterReplacer.Replace, Func>(property, property.Parameters[0], parameter); + if (oneOf.Count == 1) + { + var value = oneOf[0]; + if (typeof(TProperty).IsValueType) + { + return Expression.Lambda>(Expression.Equal(property.Body, Expression.Constant(value)), parameter); + } + else + { + return Expression.Lambda>(Expression.ReferenceEqual(property.Body, Expression.Constant(value)), parameter); + } + } + + var containsMethodInfo = _containsQueryCache.GetOrAdd(typeof(TProperty), static (key) => _containsMethodGenericCache.MakeGenericMethod(key)); + + if (oneOf.Count < 4) // arbitrary value choosen. + { + // if we have 3 or fewer values to check against its faster to do a IN(const,const,const) lookup + return Expression.Lambda>(Expression.Call(null, containsMethodInfo, Expression.Constant(oneOf), property.Body), parameter); + } + + return Expression.Lambda>(Expression.Call(null, containsMethodInfo, Expression.Call(null, _efParameterInstruction.MakeGenericMethod(oneOf.GetType()), Expression.Constant(oneOf)), property.Body), parameter); + } + + internal static class ParameterReplacer + { + // Produces an expression identical to 'expression' + // except with 'source' parameter replaced with 'target' expression. + internal static Expression Replace( + Expression expression, + ParameterExpression source, + ParameterExpression target) + { + return new ParameterReplacerVisitor(source, target) + .VisitAndConvert(expression); + } + + private sealed class ParameterReplacerVisitor : ExpressionVisitor + { + private readonly ParameterExpression _source; + private readonly ParameterExpression _target; + + public ParameterReplacerVisitor(ParameterExpression source, ParameterExpression target) + { + _source = source; + _target = target; + } + + internal Expression VisitAndConvert(Expression root) + { + return (Expression)VisitLambda(root); + } + + protected override Expression VisitLambda(Expression node) + { + // Leave all parameters alone except the one we want to replace. + var parameters = node.Parameters.Select(p => p == _source ? _target : p); + + return Expression.Lambda(Visit(node.Body), parameters); + } + + protected override Expression VisitParameter(ParameterExpression node) + { + // Replace the source with the target, visit other params as usual. + return node == _source ? _target : base.VisitParameter(node); + } + } + } +} From 66371021624223645db84858635fbf4f0112736d Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 27 Apr 2025 21:30:02 +0800 Subject: [PATCH 229/508] Correctly handle retry when I frame only failed --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 2c57cf8710..80dd080d52 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -767,7 +767,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // The mpegts demuxer cannot seek to keyframes, so we have to let the // decoder discard non-keyframes, which may contain corrupted images. var seekMpegTs = offset.HasValue && string.Equals("mpegts", container, StringComparison.OrdinalIgnoreCase); - if ((useIFrame && useTradeoff) || seekMpegTs) + if (useIFrame && (useTradeoff || seekMpegTs)) { args = "-skip_frame nokey " + args; } From 77bb1a726e97e093d9249bf4f6efea30af30896c Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 27 Apr 2025 21:36:57 +0800 Subject: [PATCH 230/508] Use full range output for image --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 80dd080d52..74d5122563 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -737,12 +737,12 @@ namespace MediaBrowser.MediaEncoding.Encoder { var peak = videoStream.VideoRangeType == VideoRangeType.DOVI ? "400" : "100"; enableHdrExtraction = true; - filters.Add($"tonemapx=tonemap=bt2390:desat=0:peak={peak}:t=bt709:m=bt709:p=bt709:format=yuv420p"); + filters.Add($"tonemapx=tonemap=bt2390:desat=0:peak={peak}:t=bt709:m=bt709:p=bt709:format=yuv420p:range=full"); } else if (SupportsFilter("zscale") && videoStream.VideoRangeType != VideoRangeType.DOVI) { enableHdrExtraction = true; - filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p"); + filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709:out_range=full,format=yuv420p"); } } From f436743f9fc122ad8c247f2488c6e888e80cecff Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 27 Apr 2025 21:37:44 +0800 Subject: [PATCH 231/508] Don't trim image extractor option --- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 74d5122563..bf574a06ff 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -756,7 +756,7 @@ namespace MediaBrowser.MediaEncoding.Encoder _threads, vf, isAudio ? string.Empty : GetImageResolutionParameter(), - EncodingHelper.GetVideoSyncOption("-1", EncoderVersion).Trim(), // auto decide fps mode + EncodingHelper.GetVideoSyncOption("-1", EncoderVersion), // auto decide fps mode tempExtractPath); if (offset.HasValue) From 2b854fb365436a4e66416916f054daad82e2fc76 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Sun, 27 Apr 2025 18:02:42 -0600 Subject: [PATCH 232/508] Add new nuget packages to abi diff --- .github/workflows/ci-compat.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index b3be082e0d..7dfcd94773 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -105,7 +105,7 @@ jobs: run: | { echo 'body< Date: Sun, 27 Apr 2025 18:11:22 -0600 Subject: [PATCH 233/508] Add required nuget properties --- .../Jellyfin.Database.Implementations.csproj | 13 +++++++++++++ .../Jellyfin.MediaEncoding.Keyframes.csproj | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj index 3b619cce61..356f96fc90 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj +++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj @@ -10,6 +10,19 @@ + + Jellyfin Contributors + Jellyfin.Database.Implementations + 10.11.0 + https://github.com/jellyfin/jellyfin + GPL-3.0-only + + + + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj index c826d3d9cc..cc8d942ebb 100644 --- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj +++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj @@ -5,6 +5,19 @@ true + + Jellyfin Contributors + Jellyfin.MediaEncoding.Keyframes + 10.11.0 + https://github.com/jellyfin/jellyfin + GPL-3.0-only + + + + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + From e66c76fc3405512b90735b5669278410f7974b1f Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 28 Apr 2025 03:18:08 +0300 Subject: [PATCH 234/508] Unified migration handling (#13950) --- .../ApplicationHost.cs | 15 -- .../Migrations/IAsyncMigrationRoutine.cs | 31 +++ .../Migrations/IDatabaseMigrationRoutine.cs | 2 + .../Migrations/IMigrationRoutine.cs | 32 --- .../Migrations/JellyfinMigrationAttribute.cs | 65 ++++++ .../Migrations/JellyfinMigrationService.cs | 219 ++++++++++++++++++ Jellyfin.Server/Migrations/MigrationRunner.cs | 204 ---------------- .../Migrations/MigrationsFactory.cs | 20 -- .../Migrations/MigrationsListStore.cs | 24 -- .../CreateNetworkConfiguration.cs | 12 +- .../MigrateEncodingOptions.cs | 12 +- .../MigrateMusicBrainzTimeout.cs | 12 +- .../MigrateNetworkConfiguration.cs | 10 +- .../RenameEnableGroupingIntoCollections.cs | 12 +- .../Routines/AddDefaultCastReceivers.cs | 12 +- .../Routines/AddDefaultPluginRepository.cs | 12 +- .../Routines/CreateUserLoggingConfigFile.cs | 12 +- .../Routines/DisableTranscodingThrottling.cs | 12 +- .../Migrations/Routines/FixAudioData.cs | 12 +- .../Migrations/Routines/FixPlaylistOwner.cs | 12 +- .../Routines/MigrateActivityLogDb.cs | 12 +- .../Routines/MigrateAuthenticationDb.cs | 12 +- .../Routines/MigrateDisplayPreferencesDb.cs | 12 +- .../Routines/MigrateKeyframeData.cs | 10 +- .../Migrations/Routines/MigrateLibraryDb.cs | 31 ++- .../Routines/MigrateRatingLevels.cs | 10 +- .../Migrations/Routines/MigrateUserDb.cs | 12 +- .../Migrations/Routines/MoveExtractedFiles.cs | 12 +- .../Migrations/Routines/MoveTrickplayFiles.cs | 14 +- .../Routines/ReaddDefaultPluginRepository.cs | 12 +- .../Routines/RemoveDownloadImagesInAdvance.cs | 12 +- .../Routines/RemoveDuplicateExtras.cs | 12 +- .../RemoveDuplicatePlaylistChildren.cs | 12 +- .../Routines/UpdateDefaultPluginRepository.cs | 12 +- .../Migrations/Stages/CodeMigration.cs | 51 ++++ .../Stages/JellyfinMigrationStageTypes.cs | 26 +++ .../Migrations/Stages/MigrationStage.cs | 16 ++ Jellyfin.Server/Program.cs | 53 ++++- .../SqliteDatabaseProvider.cs | 5 + .../JellyfinApplicationFactory.cs | 5 + 40 files changed, 555 insertions(+), 528 deletions(-) create mode 100644 Jellyfin.Server/Migrations/IAsyncMigrationRoutine.cs delete mode 100644 Jellyfin.Server/Migrations/IMigrationRoutine.cs create mode 100644 Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs create mode 100644 Jellyfin.Server/Migrations/JellyfinMigrationService.cs delete mode 100644 Jellyfin.Server/Migrations/MigrationRunner.cs delete mode 100644 Jellyfin.Server/Migrations/MigrationsFactory.cs delete mode 100644 Jellyfin.Server/Migrations/MigrationsListStore.cs create mode 100644 Jellyfin.Server/Migrations/Stages/CodeMigration.cs create mode 100644 Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs create mode 100644 Jellyfin.Server/Migrations/Stages/MigrationStage.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7b07243da7..fa6e9ff977 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -580,21 +580,6 @@ namespace Emby.Server.Implementations /// A task representing the service initialization operation. public async Task InitializeServices(IConfiguration startupConfig) { - var factory = Resolve>(); - var provider = Resolve(); - provider.DbContextFactory = factory; - - var jellyfinDb = await factory.CreateDbContextAsync().ConfigureAwait(false); - await using (jellyfinDb.ConfigureAwait(false)) - { - if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any()) - { - Logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)"); - await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false); - Logger.LogInformation("EFCore migrations applied successfully"); - } - } - var localizationManager = (LocalizationManager)Resolve(); await localizationManager.LoadAll().ConfigureAwait(false); diff --git a/Jellyfin.Server/Migrations/IAsyncMigrationRoutine.cs b/Jellyfin.Server/Migrations/IAsyncMigrationRoutine.cs new file mode 100644 index 0000000000..5b6a5fe942 --- /dev/null +++ b/Jellyfin.Server/Migrations/IAsyncMigrationRoutine.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Jellyfin.Server.Migrations; + +/// +/// Interface that describes a migration routine. +/// +internal interface IAsyncMigrationRoutine +{ + /// + /// Execute the migration routine. + /// + /// A cancellation token triggered if the migration should be aborted. + /// A representing the asynchronous operation. + public Task PerformAsync(CancellationToken cancellationToken); +} + +/// +/// Interface that describes a migration routine. +/// +[Obsolete("Use IAsyncMigrationRoutine instead")] +internal interface IMigrationRoutine +{ + /// + /// Execute the migration routine. + /// + [Obsolete("Use IAsyncMigrationRoutine.PerformAsync instead")] + public void Perform(); +} diff --git a/Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs b/Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs index 78ff1e3fd0..d2d80a81eb 100644 --- a/Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs +++ b/Jellyfin.Server/Migrations/IDatabaseMigrationRoutine.cs @@ -7,6 +7,8 @@ namespace Jellyfin.Server.Migrations; /// /// Defines a migration that operates on the Database. /// +#pragma warning disable CS0618 // Type or member is obsolete internal interface IDatabaseMigrationRoutine : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { } diff --git a/Jellyfin.Server/Migrations/IMigrationRoutine.cs b/Jellyfin.Server/Migrations/IMigrationRoutine.cs deleted file mode 100644 index 29f681df52..0000000000 --- a/Jellyfin.Server/Migrations/IMigrationRoutine.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Jellyfin.Server.Implementations; -using Microsoft.EntityFrameworkCore.Internal; - -namespace Jellyfin.Server.Migrations -{ - /// - /// Interface that describes a migration routine. - /// - internal interface IMigrationRoutine - { - /// - /// Gets the unique id for this migration. This should never be modified after the migration has been created. - /// - public Guid Id { get; } - - /// - /// Gets the display name of the migration. - /// - public string Name { get; } - - /// - /// Gets a value indicating whether to perform migration on a new install. - /// - public bool PerformOnNewInstall { get; } - - /// - /// Execute the migration routine. - /// - public void Perform(); - } -} diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs b/Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs new file mode 100644 index 0000000000..f523bc76c1 --- /dev/null +++ b/Jellyfin.Server/Migrations/JellyfinMigrationAttribute.cs @@ -0,0 +1,65 @@ +#pragma warning disable CA1019 // Define accessors for attribute arguments + +using System; +using System.Globalization; +using Jellyfin.Server.Migrations.Stages; + +namespace Jellyfin.Server.Migrations; + +/// +/// Declares an class as an migration with its set metadata. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] +public sealed class JellyfinMigrationAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The ordering this migration should be applied to. Must be a valid DateTime ISO8601 formatted string. + /// The name of this Migration. + public JellyfinMigrationAttribute(string order, string name) : this(order, name, null) + { + } + + /// + /// Initializes a new instance of the class for legacy migrations. + /// + /// The ordering this migration should be applied to. Must be a valid DateTime ISO8601 formatted string. + /// The name of this Migration. + /// [ONLY FOR LEGACY MIGRATIONS]The unique key of this migration. Must be a valid Guid formatted string. + public JellyfinMigrationAttribute(string order, string name, string? key) + { + Order = DateTime.Parse(order, CultureInfo.InvariantCulture); + Name = name; + Stage = JellyfinMigrationStageTypes.AppInitialisation; + if (key is not null) + { + Key = Guid.Parse(key); + } + } + + /// + /// Gets or Sets a value indicating whether the annoated migration should be executed on a fresh install. + /// + public bool RunMigrationOnSetup { get; set; } + + /// + /// Gets or Sets the stage the annoated migration should be executed at. Defaults to . + /// + public JellyfinMigrationStageTypes Stage { get; set; } = JellyfinMigrationStageTypes.CoreInitialisaition; + + /// + /// Gets the ordering of the migration. + /// + public DateTime Order { get; } + + /// + /// Gets the name of the migration. + /// + public string Name { get; } + + /// + /// Gets the Legacy Key of the migration. Not required for new Migrations. + /// + public Guid? Key { get; } +} diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs new file mode 100644 index 0000000000..46c22d16cc --- /dev/null +++ b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.Implementations.Serialization; +using Jellyfin.Database.Implementations; +using Jellyfin.Server.Migrations.Stages; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Migrations; + +/// +/// Handles Migration of the Jellyfin data structure. +/// +internal class JellyfinMigrationService +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly ILoggerFactory _loggerFactory; + + /// + /// Initializes a new instance of the class. + /// + /// Provides access to the jellyfin database. + /// The logger factory. + public JellyfinMigrationService(IDbContextFactory dbContextFactory, ILoggerFactory loggerFactory) + { + _dbContextFactory = dbContextFactory; + _loggerFactory = loggerFactory; +#pragma warning disable CS0618 // Type or member is obsolete + Migrations = [.. typeof(IMigrationRoutine).Assembly.GetTypes().Where(e => typeof(IMigrationRoutine).IsAssignableFrom(e) || typeof(IAsyncMigrationRoutine).IsAssignableFrom(e)) + .Select(e => (Type: e, Metadata: e.GetCustomAttribute())) + .Where(e => e.Metadata != null) + .GroupBy(e => e.Metadata!.Stage) + .Select(f => + { + var stage = new MigrationStage(f.Key); + foreach (var item in f) + { + stage.Add(new(item.Type, item.Metadata!)); + } + + return stage; + })]; +#pragma warning restore CS0618 // Type or member is obsolete + } + + private interface IInternalMigration + { + Task PerformAsync(ILogger logger); + } + + private HashSet Migrations { get; set; } + + public async Task CheckFirstTimeRunOrMigration(IApplicationPaths appPaths) + { + var logger = _loggerFactory.CreateLogger(); + logger.LogInformation("Initialise Migration service."); + var xmlSerializer = new MyXmlSerializer(); + var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath) + ? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)! + : new ServerConfiguration(); + if (!serverConfig.IsStartupWizardCompleted) + { + logger.LogInformation("System initialisation detected. Seed data."); + var flatApplyMigrations = Migrations.SelectMany(e => e.Where(f => !f.Metadata.RunMigrationOnSetup)).ToArray(); + + var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var historyRepository = dbContext.GetService(); + + await historyRepository.CreateIfNotExistsAsync().ConfigureAwait(false); + var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false); + var startupScripts = flatApplyMigrations + .Where(e => !appliedMigrations.Any(f => f != e.BuildCodeMigrationId())) + .Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion())))) + .ToArray(); + foreach (var item in startupScripts) + { + logger.LogInformation("Seed migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name); + await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false); + } + } + + logger.LogInformation("Migration system initialisation completed."); + } + else + { + // migrate any existing migration.xml files + var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, "migrations.xml"); + var migrationOptions = File.Exists(migrationConfigPath) + ? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)! + : null; + if (migrationOptions != null && migrationOptions.Applied.Count > 0) + { + logger.LogInformation("Old migration style migration.xml detected. Migrate now."); + var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var historyRepository = dbContext.GetService(); + var appliedMigrations = await dbContext.Database.GetAppliedMigrationsAsync().ConfigureAwait(false); + var oldMigrations = Migrations.SelectMany(e => e) + .Where(e => migrationOptions.Applied.Any(f => f.Id.Equals(e.Metadata.Key!.Value))) // this is a legacy migration that will always have its own ID. + .Where(e => !appliedMigrations.Contains(e.BuildCodeMigrationId())) + .ToArray(); + var startupScripts = oldMigrations.Select(e => (Migration: e.Metadata, Script: historyRepository.GetInsertScript(new HistoryRow(e.BuildCodeMigrationId(), GetJellyfinVersion())))); + foreach (var item in startupScripts) + { + logger.LogInformation("Migrate migration {Key}-{Name}.", item.Migration.Key, item.Migration.Name); + await dbContext.Database.ExecuteSqlRawAsync(item.Script).ConfigureAwait(false); + } + + logger.LogInformation("Rename old migration.xml to migration.xml.backup"); + File.Move(migrationConfigPath, Path.ChangeExtension(migrationConfigPath, ".xml.backup"), true); + } + } + } + } + + public async Task MigrateStepAsync(JellyfinMigrationStageTypes stage, IServiceProvider? serviceProvider) + { + var logger = _loggerFactory.CreateLogger(); + logger.LogInformation("Migrate stage {Stage}.", stage); + ICollection migrationStage = (Migrations.FirstOrDefault(e => e.Stage == stage) as ICollection) ?? []; + + var dbContext = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) + { + var historyRepository = dbContext.GetService(); + var migrationsAssembly = dbContext.GetService(); + var appliedMigrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false); + var pendingCodeMigrations = migrationStage + .Where(e => appliedMigrations.All(f => f.MigrationId != e.BuildCodeMigrationId())) + .Select(e => (Key: e.BuildCodeMigrationId(), Migration: new InternalCodeMigration(e, serviceProvider, dbContext))) + .ToArray(); + + (string Key, InternalDatabaseMigration Migration)[] pendingDatabaseMigrations = []; + if (stage is JellyfinMigrationStageTypes.CoreInitialisaition) + { + pendingDatabaseMigrations = migrationsAssembly.Migrations.Where(f => appliedMigrations.All(e => e.MigrationId != f.Key)) + .Select(e => (Key: e.Key, Migration: new InternalDatabaseMigration(e, dbContext))) + .ToArray(); + } + + (string Key, IInternalMigration Migration)[] pendingMigrations = [.. pendingCodeMigrations, .. pendingDatabaseMigrations]; + logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingCodeMigrations.Length, stage); + var migrations = pendingMigrations.OrderBy(e => e.Key).ToArray(); + foreach (var item in migrations) + { + try + { + logger.LogInformation("Perform migration {Name}", item.Key); + await item.Migration.PerformAsync(_loggerFactory.CreateLogger(item.GetType().Name)).ConfigureAwait(false); + logger.LogInformation("Migration {Name} was successfully applied", item.Key); + } + catch (Exception ex) + { + logger.LogCritical(ex, "Migration {Name} failed", item.Key); + throw; + } + } + } + } + + private static string GetJellyfinVersion() + { + return Assembly.GetEntryAssembly()!.GetName().Version!.ToString(); + } + + private class InternalCodeMigration : IInternalMigration + { + private readonly CodeMigration _codeMigration; + private readonly IServiceProvider? _serviceProvider; + private JellyfinDbContext _dbContext; + + public InternalCodeMigration(CodeMigration codeMigration, IServiceProvider? serviceProvider, JellyfinDbContext dbContext) + { + _codeMigration = codeMigration; + _serviceProvider = serviceProvider; + _dbContext = dbContext; + } + + public async Task PerformAsync(ILogger logger) + { + await _codeMigration.Perform(_serviceProvider, CancellationToken.None).ConfigureAwait(false); + + var historyRepository = _dbContext.GetService(); + var createScript = historyRepository.GetInsertScript(new HistoryRow(_codeMigration.BuildCodeMigrationId(), GetJellyfinVersion())); + await _dbContext.Database.ExecuteSqlRawAsync(createScript).ConfigureAwait(false); + } + } + + private class InternalDatabaseMigration : IInternalMigration + { + private readonly JellyfinDbContext _jellyfinDbContext; + private KeyValuePair _databaseMigrationInfo; + + public InternalDatabaseMigration(KeyValuePair databaseMigrationInfo, JellyfinDbContext jellyfinDbContext) + { + _databaseMigrationInfo = databaseMigrationInfo; + _jellyfinDbContext = jellyfinDbContext; + } + + public async Task PerformAsync(ILogger logger) + { + var migrator = _jellyfinDbContext.GetService(); + await migrator.MigrateAsync(_databaseMigrationInfo.Key).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs deleted file mode 100644 index c223576dad..0000000000 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations; -using Emby.Server.Implementations.Serialization; -using Jellyfin.Database.Implementations; -using Jellyfin.Server.Implementations; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Configuration; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Jellyfin.Server.Migrations -{ - /// - /// The class that knows which migrations to apply and how to apply them. - /// - public sealed class MigrationRunner - { - /// - /// The list of known pre-startup migrations, in order of applicability. - /// - private static readonly Type[] _preStartupMigrationTypes = - { - typeof(PreStartupRoutines.CreateNetworkConfiguration), - typeof(PreStartupRoutines.MigrateMusicBrainzTimeout), - typeof(PreStartupRoutines.MigrateNetworkConfiguration), - typeof(PreStartupRoutines.MigrateEncodingOptions), - typeof(PreStartupRoutines.RenameEnableGroupingIntoCollections) - }; - - /// - /// The list of known migrations, in order of applicability. - /// - private static readonly Type[] _migrationTypes = - { - typeof(Routines.DisableTranscodingThrottling), - typeof(Routines.CreateUserLoggingConfigFile), - typeof(Routines.MigrateActivityLogDb), - typeof(Routines.RemoveDuplicateExtras), - typeof(Routines.AddDefaultPluginRepository), - typeof(Routines.MigrateUserDb), - typeof(Routines.ReaddDefaultPluginRepository), - typeof(Routines.MigrateDisplayPreferencesDb), - typeof(Routines.RemoveDownloadImagesInAdvance), - typeof(Routines.MigrateAuthenticationDb), - typeof(Routines.FixPlaylistOwner), - typeof(Routines.AddDefaultCastReceivers), - typeof(Routines.UpdateDefaultPluginRepository), - typeof(Routines.FixAudioData), - typeof(Routines.RemoveDuplicatePlaylistChildren), - typeof(Routines.MigrateLibraryDb), - typeof(Routines.MoveExtractedFiles), - typeof(Routines.MigrateRatingLevels), - typeof(Routines.MoveTrickplayFiles), - typeof(Routines.MigrateKeyframeData), - }; - - /// - /// Run all needed migrations. - /// - /// CoreAppHost that hosts current version. - /// Factory for making the logger. - /// A representing the asynchronous operation. - public static async Task Run(CoreAppHost host, ILoggerFactory loggerFactory) - { - var logger = loggerFactory.CreateLogger(); - var migrations = _migrationTypes - .Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m)) - .OfType() - .ToArray(); - - var migrationOptions = host.ConfigurationManager.GetConfiguration(MigrationsListStore.StoreKey); - HandleStartupWizardCondition(migrations, migrationOptions, host.ConfigurationManager.Configuration.IsStartupWizardCompleted, logger); - await PerformMigrations(migrations, migrationOptions, options => host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, options), logger, host.ServiceProvider.GetRequiredService()) - .ConfigureAwait(false); - } - - /// - /// Run all needed pre-startup migrations. - /// - /// Application paths. - /// Factory for making the logger. - /// A representing the asynchronous operation. - public static async Task RunPreStartup(ServerApplicationPaths appPaths, ILoggerFactory loggerFactory) - { - var logger = loggerFactory.CreateLogger(); - var migrations = _preStartupMigrationTypes - .Select(m => Activator.CreateInstance(m, appPaths, loggerFactory)) - .OfType() - .ToArray(); - - var xmlSerializer = new MyXmlSerializer(); - var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, MigrationsListStore.StoreKey.ToLowerInvariant() + ".xml"); - var migrationOptions = File.Exists(migrationConfigPath) - ? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)! - : new MigrationOptions(); - - // We have to deserialize it manually since the configuration manager may overwrite it - var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath) - ? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)! - : new ServerConfiguration(); - - HandleStartupWizardCondition(migrations, migrationOptions, serverConfig.IsStartupWizardCompleted, logger); - await PerformMigrations(migrations, migrationOptions, options => xmlSerializer.SerializeToFile(options, migrationConfigPath), logger, null).ConfigureAwait(false); - } - - private static void HandleStartupWizardCondition(IEnumerable migrations, MigrationOptions migrationOptions, bool isStartWizardCompleted, ILogger logger) - { - if (isStartWizardCompleted) - { - return; - } - - // If startup wizard is not finished, this is a fresh install. - var onlyOldInstalls = migrations.Where(m => !m.PerformOnNewInstall).ToArray(); - logger.LogInformation("Marking following migrations as applied because this is a fresh install: {@OnlyOldInstalls}", onlyOldInstalls.Select(m => m.Name)); - migrationOptions.Applied.AddRange(onlyOldInstalls.Select(m => (m.Id, m.Name))); - } - - private static async Task PerformMigrations( - IMigrationRoutine[] migrations, - MigrationOptions migrationOptions, - Action saveConfiguration, - ILogger logger, - IJellyfinDatabaseProvider? jellyfinDatabaseProvider) - { - // save already applied migrations, and skip them thereafter - saveConfiguration(migrationOptions); - var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet(); - var migrationsToBeApplied = migrations.Where(e => !appliedMigrationIds.Contains(e.Id)).ToArray(); - - string? migrationKey = null; - if (jellyfinDatabaseProvider is not null && migrationsToBeApplied.Any(f => f is IDatabaseMigrationRoutine)) - { - logger.LogInformation("Performing database backup"); - try - { - migrationKey = await jellyfinDatabaseProvider.MigrationBackupFast(CancellationToken.None).ConfigureAwait(false); - logger.LogInformation("Database backup with key '{BackupKey}' has been successfully created.", migrationKey); - } - catch (NotImplementedException) - { - logger.LogWarning("Could not perform backup of database before migration because provider does not support it"); - } - } - - List databaseMigrations = []; - try - { - foreach (var migrationRoutine in migrationsToBeApplied) - { - logger.LogInformation("Applying migration '{Name}'", migrationRoutine.Name); - var isDbMigration = migrationRoutine is IDatabaseMigrationRoutine; - - if (isDbMigration) - { - databaseMigrations.Add(migrationRoutine); - } - - try - { - migrationRoutine.Perform(); - } - catch (Exception ex) - { - logger.LogError(ex, "Could not apply migration '{Name}'", migrationRoutine.Name); - throw; - } - - // Mark the migration as completed - logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name); - if (!isDbMigration) - { - migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name)); - saveConfiguration(migrationOptions); - logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name); - } - } - } - catch (Exception) when (migrationKey is not null && jellyfinDatabaseProvider is not null) - { - if (databaseMigrations.Count != 0) - { - logger.LogInformation("Rolling back database as migrations reported failure."); - await jellyfinDatabaseProvider.RestoreBackupFast(migrationKey, CancellationToken.None).ConfigureAwait(false); - } - - throw; - } - - foreach (var migrationRoutine in databaseMigrations) - { - migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name)); - saveConfiguration(migrationOptions); - logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name); - } - } - } -} diff --git a/Jellyfin.Server/Migrations/MigrationsFactory.cs b/Jellyfin.Server/Migrations/MigrationsFactory.cs deleted file mode 100644 index 23c1b1ee6f..0000000000 --- a/Jellyfin.Server/Migrations/MigrationsFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Common.Configuration; - -namespace Jellyfin.Server.Migrations -{ - /// - /// A factory that can find a persistent file of the migration configuration, which lists all applied migrations. - /// - public class MigrationsFactory : IConfigurationFactory - { - /// - public IEnumerable GetConfigurations() - { - return new[] - { - new MigrationsListStore() - }; - } - } -} diff --git a/Jellyfin.Server/Migrations/MigrationsListStore.cs b/Jellyfin.Server/Migrations/MigrationsListStore.cs deleted file mode 100644 index 7a1ca66714..0000000000 --- a/Jellyfin.Server/Migrations/MigrationsListStore.cs +++ /dev/null @@ -1,24 +0,0 @@ -using MediaBrowser.Common.Configuration; - -namespace Jellyfin.Server.Migrations -{ - /// - /// A configuration that lists all the migration routines that were applied. - /// - public class MigrationsListStore : ConfigurationStore - { - /// - /// The name of the configuration in the storage. - /// - public static readonly string StoreKey = "migrations"; - - /// - /// Initializes a new instance of the class. - /// - public MigrationsListStore() - { - ConfigurationType = typeof(MigrationOptions); - Key = StoreKey; - } - } -} diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs index 8462d0a8c9..a62523b88f 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs @@ -8,7 +8,10 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.PreStartupRoutines; /// +[JellyfinMigration("2025-04-20T00:00:00", nameof(CreateNetworkConfiguration), "9B354818-94D5-4B68-AC49-E35CB85F9D84", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)] +#pragma warning disable CS0618 // Type or member is obsolete public class CreateNetworkConfiguration : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private readonly ServerApplicationPaths _applicationPaths; private readonly ILogger _logger; @@ -24,15 +27,6 @@ public class CreateNetworkConfiguration : IMigrationRoutine _logger = loggerFactory.CreateLogger(); } - /// - public Guid Id => Guid.Parse("9B354818-94D5-4B68-AC49-E35CB85F9D84"); - - /// - public string Name => nameof(CreateNetworkConfiguration); - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs index 61f5620dc0..3455696994 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateEncodingOptions.cs @@ -10,7 +10,10 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.PreStartupRoutines; /// +[JellyfinMigration("2025-04-20T03:00:00", nameof(MigrateEncodingOptions), "A8E61960-7726-4450-8F3D-82C12DAABBCB", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)] +#pragma warning disable CS0618 // Type or member is obsolete public class MigrateEncodingOptions : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private readonly ServerApplicationPaths _applicationPaths; private readonly ILogger _logger; @@ -26,15 +29,6 @@ public class MigrateEncodingOptions : IMigrationRoutine _logger = loggerFactory.CreateLogger(); } - /// - public Guid Id => Guid.Parse("A8E61960-7726-4450-8F3D-82C12DAABBCB"); - - /// - public string Name => nameof(MigrateEncodingOptions); - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs index 580282a5f5..bdbf0c1ce4 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateMusicBrainzTimeout.cs @@ -9,7 +9,10 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.PreStartupRoutines; /// +[JellyfinMigration("2025-04-20T02:00:00", nameof(MigrateMusicBrainzTimeout), "A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)] +#pragma warning disable CS0618 // Type or member is obsolete public class MigrateMusicBrainzTimeout : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private readonly ServerApplicationPaths _applicationPaths; private readonly ILogger _logger; @@ -25,15 +28,6 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine _logger = loggerFactory.CreateLogger(); } - /// - public Guid Id => Guid.Parse("A6DCACF4-C057-4Ef9-80D3-61CEF9DDB4F0"); - - /// - public string Name => nameof(MigrateMusicBrainzTimeout); - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs index 09b2921714..f2790c1a1f 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/MigrateNetworkConfiguration.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.PreStartupRoutines; /// +[JellyfinMigration("2025-04-20T01:00:00", nameof(MigrateNetworkConfiguration), "4FB5C950-1991-11EE-9B4B-0800200C9A66", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)] public class MigrateNetworkConfiguration : IMigrationRoutine { private readonly ServerApplicationPaths _applicationPaths; @@ -27,15 +28,6 @@ public class MigrateNetworkConfiguration : IMigrationRoutine _logger = loggerFactory.CreateLogger(); } - /// - public Guid Id => Guid.Parse("4FB5C950-1991-11EE-9B4B-0800200C9A66"); - - /// - public string Name => nameof(MigrateNetworkConfiguration); - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs b/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs index 0a37b35a6a..c0ca7896fb 100644 --- a/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs +++ b/Jellyfin.Server/Migrations/PreStartupRoutines/RenameEnableGroupingIntoCollections.cs @@ -9,7 +9,10 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.PreStartupRoutines; /// +[JellyfinMigration("2025-04-20T04:00:00", nameof(RenameEnableGroupingIntoCollections), "E73B777D-CD5C-4E71-957A-B86B3660B7CF", Stage = Stages.JellyfinMigrationStageTypes.PreInitialisation)] +#pragma warning disable CS0618 // Type or member is obsolete public class RenameEnableGroupingIntoCollections : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private readonly ServerApplicationPaths _applicationPaths; private readonly ILogger _logger; @@ -25,15 +28,6 @@ public class RenameEnableGroupingIntoCollections : IMigrationRoutine _logger = loggerFactory.CreateLogger(); } - /// - public Guid Id => Guid.Parse("E73B777D-CD5C-4E71-957A-B86B3660B7CF"); - - /// - public string Name => nameof(RenameEnableGroupingIntoCollections); - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs index 2047ec743e..7e92433423 100644 --- a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs +++ b/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs @@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines; /// /// Migration to add the default cast receivers to the system config. /// +[JellyfinMigration("2025-04-20T16:00:00", nameof(AddDefaultCastReceivers), "34A1A1C4-5572-418E-A2F8-32CDFE2668E8", RunMigrationOnSetup = true)] +#pragma warning disable CS0618 // Type or member is obsolete public class AddDefaultCastReceivers : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private readonly IServerConfigurationManager _serverConfigurationManager; @@ -20,15 +23,6 @@ public class AddDefaultCastReceivers : IMigrationRoutine _serverConfigurationManager = serverConfigurationManager; } - /// - public Guid Id => new("34A1A1C4-5572-418E-A2F8-32CDFE2668E8"); - - /// - public string Name => "AddDefaultCastReceivers"; - - /// - public bool PerformOnNewInstall => true; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs index fc6b5d5979..603e01c180 100644 --- a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs +++ b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs @@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines /// /// Migration to initialize system configuration with the default plugin repository. /// + [JellyfinMigration("2025-04-20T09:00:00", nameof(AddDefaultPluginRepository), "EB58EBEE-9514-4B9B-8225-12E1A40020DF", RunMigrationOnSetup = true)] +#pragma warning disable CS0618 // Type or member is obsolete public class AddDefaultPluginRepository : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private readonly IServerConfigurationManager _serverConfigurationManager; @@ -26,15 +29,6 @@ namespace Jellyfin.Server.Migrations.Routines _serverConfigurationManager = serverConfigurationManager; } - /// - public Guid Id => Guid.Parse("EB58EBEE-9514-4B9B-8225-12E1A40020DF"); - - /// - public string Name => "AddDefaultPluginRepository"; - - /// - public bool PerformOnNewInstall => true; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs index 5a8ef2e1cd..9d2a901cd9 100644 --- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs +++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs @@ -12,7 +12,10 @@ namespace Jellyfin.Server.Migrations.Routines /// If the deprecated logging.json file exists and has a custom config, it will be used as logging.user.json, /// otherwise a blank file will be created. /// + [JellyfinMigration("2025-04-20T06:00:00", nameof(CreateUserLoggingConfigFile), "EF103419-8451-40D8-9F34-D1A8E93A1679")] +#pragma warning disable CS0618 // Type or member is obsolete internal class CreateUserLoggingConfigFile : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { /// /// File history for logging.json as existed during this migration creation. The contents for each has been minified. @@ -42,15 +45,6 @@ namespace Jellyfin.Server.Migrations.Routines _appPaths = appPaths; } - /// - public Guid Id => Guid.Parse("{EF103419-8451-40D8-9F34-D1A8E93A1679}"); - - /// - public string Name => "CreateLoggingConfigHierarchy"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs index 378e88e25b..ca9bf32648 100644 --- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs +++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs @@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines /// /// Disable transcode throttling for all installations since it is currently broken for certain video formats. /// + [JellyfinMigration("2025-04-20T05:00:00", nameof(DisableTranscodingThrottling), "4124C2CD-E939-4FFB-9BE9-9B311C413638")] +#pragma warning disable CS0618 // Type or member is obsolete internal class DisableTranscodingThrottling : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private readonly ILogger _logger; private readonly IConfigurationManager _configManager; @@ -18,15 +21,6 @@ namespace Jellyfin.Server.Migrations.Routines _configManager = configManager; } - /// - public Guid Id => Guid.Parse("{4124C2CD-E939-4FFB-9BE9-9B311C413638}"); - - /// - public string Name => "DisableTranscodingThrottling"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs b/Jellyfin.Server/Migrations/Routines/FixAudioData.cs index a202533692..6ebb5000e4 100644 --- a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs +++ b/Jellyfin.Server/Migrations/Routines/FixAudioData.cs @@ -16,7 +16,10 @@ namespace Jellyfin.Server.Migrations.Routines /// /// Fixes the data column of audio types to be deserializable. /// + [JellyfinMigration("2025-04-20T18:00:00", nameof(FixAudioData), "CF6FABC2-9FBE-4933-84A5-FFE52EF22A58")] +#pragma warning disable CS0618 // Type or member is obsolete internal class FixAudioData : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private const string DbFilename = "library.db"; private readonly ILogger _logger; @@ -33,15 +36,6 @@ namespace Jellyfin.Server.Migrations.Routines _logger = loggerFactory.CreateLogger(); } - /// - public Guid Id => Guid.Parse("{CF6FABC2-9FBE-4933-84A5-FFE52EF22A58}"); - - /// - public string Name => "FixAudioData"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs index 192c170b26..f31c1afbd3 100644 --- a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs +++ b/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs @@ -13,7 +13,10 @@ namespace Jellyfin.Server.Migrations.Routines; /// /// Properly set playlist owner. /// +[JellyfinMigration("2025-04-20T15:00:00", nameof(FixPlaylistOwner), "615DFA9E-2497-4DBB-A472-61938B752C5B")] +#pragma warning disable CS0618 // Type or member is obsolete internal class FixPlaylistOwner : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; @@ -29,15 +32,6 @@ internal class FixPlaylistOwner : IMigrationRoutine _playlistManager = playlistManager; } - /// - public Guid Id => Guid.Parse("{615DFA9E-2497-4DBB-A472-61938B752C5B}"); - - /// - public string Name => "FixPlaylistOwner"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index e9fe9abceb..14089cac75 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -14,7 +14,10 @@ namespace Jellyfin.Server.Migrations.Routines /// /// The migration routine for migrating the activity log database to EF Core. /// + [JellyfinMigration("2025-04-20T07:00:00", nameof(MigrateActivityLogDb), "3793eb59-bc8c-456c-8b9f-bd5a62a42978")] +#pragma warning disable CS0618 // Type or member is obsolete public class MigrateActivityLogDb : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private const string DbFilename = "activitylog.db"; @@ -35,15 +38,6 @@ namespace Jellyfin.Server.Migrations.Routines _paths = paths; } - /// - public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978"); - - /// - public string Name => "MigrateActivityLogDatabase"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs index feaf46c843..e4362f44da 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs @@ -15,7 +15,10 @@ namespace Jellyfin.Server.Migrations.Routines /// /// A migration that moves data from the authentication database into the new schema. /// + [JellyfinMigration("2025-04-20T14:00:00", nameof(MigrateAuthenticationDb), "5BD72F41-E6F3-4F60-90AA-09869ABE0E22")] +#pragma warning disable CS0618 // Type or member is obsolete public class MigrateAuthenticationDb : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private const string DbFilename = "authentication.db"; @@ -43,15 +46,6 @@ namespace Jellyfin.Server.Migrations.Routines _userManager = userManager; } - /// - public Guid Id => Guid.Parse("5BD72F41-E6F3-4F60-90AA-09869ABE0E22"); - - /// - public string Name => "MigrateAuthenticationDatabase"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index a8fa2e52a1..49ed01d6bb 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -20,7 +20,10 @@ namespace Jellyfin.Server.Migrations.Routines /// /// The migration routine for migrating the display preferences database to EF Core. /// + [JellyfinMigration("2025-04-20T12:00:00", nameof(MigrateDisplayPreferencesDb), "06387815-C3CC-421F-A888-FB5F9992BEA8")] +#pragma warning disable CS0618 // Type or member is obsolete public class MigrateDisplayPreferencesDb : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private const string DbFilename = "displaypreferences.db"; @@ -51,15 +54,6 @@ namespace Jellyfin.Server.Migrations.Routines _jsonOptions.Converters.Add(new JsonStringEnumConverter()); } - /// - public Guid Id => Guid.Parse("06387815-C3CC-421F-A888-FB5F9992BEA8"); - - /// - public string Name => "MigrateDisplayPreferencesDatabase"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs index 68d7a7b876..c5bc702789 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs @@ -19,6 +19,7 @@ namespace Jellyfin.Server.Migrations.Routines; /// /// Migration to move extracted files to the new directories. /// +[JellyfinMigration("2025-04-21T00:00:00", nameof(MigrateKeyframeData), "EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24")] public class MigrateKeyframeData : IDatabaseMigrationRoutine { private readonly ILogger _logger; @@ -44,15 +45,6 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine private string KeyframeCachePath => Path.Combine(_appPaths.DataPath, "keyframes"); - /// - public Guid Id => new("EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24"); - - /// - public string Name => "MigrateKeyframeData"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 105fd555f6..8374508e66 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -16,10 +16,19 @@ using Jellyfin.Database.Implementations.Entities; using Jellyfin.Extensions; using Jellyfin.Server.Implementations.Item; using MediaBrowser.Controller; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.IO; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using BaseItemEntity = Jellyfin.Database.Implementations.Entities.BaseItemEntity; using Chapter = Jellyfin.Database.Implementations.Entities.Chapter; @@ -29,6 +38,7 @@ namespace Jellyfin.Server.Migrations.Routines; /// /// The migration routine for migrating the userdata database to EF Core. /// +[JellyfinMigration("2025-04-20T20:00:00", nameof(MigrateLibraryDb), "36445464-849f-429f-9ad0-bb130efa0664")] internal class MigrateLibraryDb : IDatabaseMigrationRoutine { private const string DbFilename = "library.db"; @@ -45,11 +55,13 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine /// The database provider. /// The server application paths. /// The database provider for special access. + /// The Service provider. public MigrateLibraryDb( ILogger logger, IDbContextFactory provider, IServerApplicationPaths paths, - IJellyfinDatabaseProvider jellyfinDatabaseProvider) + IJellyfinDatabaseProvider jellyfinDatabaseProvider, + IServiceProvider serviceProvider) { _logger = logger; _provider = provider; @@ -57,15 +69,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine _jellyfinDatabaseProvider = jellyfinDatabaseProvider; } - /// - public Guid Id => Guid.Parse("36445464-849f-429f-9ad0-bb130efa0664"); - - /// - public string Name => "MigrateLibraryDbData"; - - /// - public bool PerformOnNewInstall => false; // TODO Change back after testing - /// public void Perform() { @@ -73,6 +76,12 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine var dataPath = _paths.DataPath; var libraryDbPath = Path.Combine(dataPath, DbFilename); + if (!File.Exists(libraryDbPath)) + { + _logger.LogError("Cannot migrate {LibraryDb} as it does not exist..", libraryDbPath); + return; + } + using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly"); var fullOperationTimer = new Stopwatch(); @@ -395,8 +404,6 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine _logger.LogInformation("Move {0} to {1}.", libraryDbPath, libraryDbPath + ".old"); File.Move(libraryDbPath, libraryDbPath + ".old", true); - - _jellyfinDatabaseProvider.RunScheduledOptimisation(CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); } private DatabaseMigrationStep GetPreparedDbContext(string operationName) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs index c38beb7232..96276e9b10 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs @@ -10,6 +10,7 @@ namespace Jellyfin.Server.Migrations.Routines /// /// Migrate rating levels. /// + [JellyfinMigration("2025-04-20T22:00:00", nameof(MigrateRatingLevels), "98724538-EB11-40E3-931A-252C55BDDE7A")] internal class MigrateRatingLevels : IDatabaseMigrationRoutine { private readonly ILogger _logger; @@ -26,15 +27,6 @@ namespace Jellyfin.Server.Migrations.Routines _logger = loggerFactory.CreateLogger(); } - /// - public Guid Id => Guid.Parse("{98724538-EB11-40E3-931A-252C55BDDE7A}"); - - /// - public string Name => "MigrateRatingLevels"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 1b5fab7a89..7a23fcc98c 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -22,7 +22,10 @@ namespace Jellyfin.Server.Migrations.Routines /// /// The migration routine for migrating the user database to EF Core. /// + [JellyfinMigration("2025-04-20T10:00:00", nameof(MigrateUserDb), "5C4B82A2-F053-4009-BD05-B6FCAD82F14C")] +#pragma warning disable CS0618 // Type or member is obsolete public class MigrateUserDb : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private const string DbFilename = "users.db"; @@ -50,15 +53,6 @@ namespace Jellyfin.Server.Migrations.Routines _xmlSerializer = xmlSerializer; } - /// - public Guid Id => Guid.Parse("5C4B82A2-F053-4009-BD05-B6FCAD82F14C"); - - /// - public string Name => "MigrateUserDatabase"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs index c5bbcd6f94..9031f2fdcf 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs @@ -24,7 +24,10 @@ namespace Jellyfin.Server.Migrations.Routines; /// /// Migration to move extracted files to the new directories. /// +[JellyfinMigration("2025-04-20T21:00:00", nameof(MoveExtractedFiles), "9063b0Ef-CFF1-4EDC-9A13-74093681A89B")] +#pragma warning disable CS0618 // Type or member is obsolete public class MoveExtractedFiles : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private readonly IApplicationPaths _appPaths; private readonly ILogger _logger; @@ -58,15 +61,6 @@ public class MoveExtractedFiles : IMigrationRoutine private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments"); - /// - public Guid Id => new("9063b0Ef-CFF1-4EDC-9A13-74093681A89B"); - - /// - public string Name => "MoveExtractedFiles"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs index a278138cee..6077080439 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs @@ -15,7 +15,10 @@ namespace Jellyfin.Server.Migrations.Routines; /// /// Migration to move trickplay files to the new directory. /// +[JellyfinMigration("2025-04-20T23:00:00", nameof(MoveTrickplayFiles), "9540D44A-D8DC-11EF-9CBB-B77274F77C52", RunMigrationOnSetup = true)] +#pragma warning disable CS0618 // Type or member is obsolete public class MoveTrickplayFiles : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private readonly ITrickplayManager _trickplayManager; private readonly IFileSystem _fileSystem; @@ -41,15 +44,6 @@ public class MoveTrickplayFiles : IMigrationRoutine _logger = logger; } - /// - public Guid Id => new("9540D44A-D8DC-11EF-9CBB-B77274F77C52"); - - /// - public string Name => "MoveTrickplayFiles"; - - /// - public bool PerformOnNewInstall => true; - /// public void Perform() { @@ -103,7 +97,7 @@ public class MoveTrickplayFiles : IMigrationRoutine offset += Limit; previousCount = trickplayInfos.Count; - _logger.LogInformation("Checked: {Checked} - Moved: {Count} - Time: {Time}", itemCount, offset, sw.Elapsed); + _logger.LogInformation("Checked: {Checked} - Moved: {Count} - Time: {Time}", offset, itemCount, sw.Elapsed); } while (previousCount == Limit); _logger.LogInformation("Moved {Count} items in {Time}", itemCount, sw.Elapsed); diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs index 9cfaec46f8..1ef1dd45fe 100644 --- a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs +++ b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs @@ -7,7 +7,10 @@ namespace Jellyfin.Server.Migrations.Routines /// /// Migration to initialize system configuration with the default plugin repository. /// + [JellyfinMigration("2025-04-20T11:00:00", nameof(ReaddDefaultPluginRepository), "5F86E7F6-D966-4C77-849D-7A7B40B68C4E", RunMigrationOnSetup = true)] +#pragma warning disable CS0618 // Type or member is obsolete public class ReaddDefaultPluginRepository : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private readonly IServerConfigurationManager _serverConfigurationManager; @@ -26,15 +29,6 @@ namespace Jellyfin.Server.Migrations.Routines _serverConfigurationManager = serverConfigurationManager; } - /// - public Guid Id => Guid.Parse("5F86E7F6-D966-4C77-849D-7A7B40B68C4E"); - - /// - public string Name => "ReaddDefaultPluginRepository"; - - /// - public bool PerformOnNewInstall => true; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs b/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs index 52fb93d594..477363e0de 100644 --- a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs +++ b/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs @@ -8,7 +8,10 @@ namespace Jellyfin.Server.Migrations.Routines /// /// Removes the old 'RemoveDownloadImagesInAdvance' from library options. /// + [JellyfinMigration("2025-04-20T13:00:00", nameof(RemoveDownloadImagesInAdvance), "A81F75E0-8F43-416F-A5E8-516CCAB4D8CC")] +#pragma warning disable CS0618 // Type or member is obsolete internal class RemoveDownloadImagesInAdvance : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; @@ -19,15 +22,6 @@ namespace Jellyfin.Server.Migrations.Routines _libraryManager = libraryManager; } - /// - public Guid Id => Guid.Parse("{A81F75E0-8F43-416F-A5E8-516CCAB4D8CC}"); - - /// - public string Name => "RemoveDownloadImagesInAdvance"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs index 7b0d9456dc..c80512deed 100644 --- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs @@ -12,7 +12,10 @@ namespace Jellyfin.Server.Migrations.Routines /// /// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself. /// + [JellyfinMigration("2025-04-20T08:00:00", nameof(RemoveDuplicateExtras), "ACBE17B7-8435-4A83-8B64-6FCF162CB9BD")] +#pragma warning disable CS0618 // Type or member is obsolete internal class RemoveDuplicateExtras : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private const string DbFilename = "library.db"; private readonly ILogger _logger; @@ -24,15 +27,6 @@ namespace Jellyfin.Server.Migrations.Routines _paths = paths; } - /// - public Guid Id => Guid.Parse("{ACBE17B7-8435-4A83-8B64-6FCF162CB9BD}"); - - /// - public string Name => "RemoveDuplicateExtras"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs index e183a1d63a..ce2be2755c 100644 --- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs @@ -11,7 +11,10 @@ namespace Jellyfin.Server.Migrations.Routines; /// /// Remove duplicate playlist entries. /// +[JellyfinMigration("2025-04-20T19:00:00", nameof(RemoveDuplicatePlaylistChildren), "96C156A2-7A13-4B3B-A8B8-FB80C94D20C0")] +#pragma warning disable CS0618 // Type or member is obsolete internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private readonly ILibraryManager _libraryManager; private readonly IPlaylistManager _playlistManager; @@ -24,15 +27,6 @@ internal class RemoveDuplicatePlaylistChildren : IMigrationRoutine _playlistManager = playlistManager; } - /// - public Guid Id => Guid.Parse("{96C156A2-7A13-4B3B-A8B8-FB80C94D20C0}"); - - /// - public string Name => "RemoveDuplicatePlaylistChildren"; - - /// - public bool PerformOnNewInstall => false; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs index 7e8c8ac871..cf3f5433b4 100644 --- a/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs +++ b/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs @@ -6,7 +6,10 @@ namespace Jellyfin.Server.Migrations.Routines; /// /// Migration to update the default Jellyfin plugin repository. /// +[JellyfinMigration("2025-04-20T17:00:00", nameof(UpdateDefaultPluginRepository), "852816E0-2712-49A9-9240-C6FC5FCAD1A8", RunMigrationOnSetup = true)] +#pragma warning disable CS0618 // Type or member is obsolete public class UpdateDefaultPluginRepository : IMigrationRoutine +#pragma warning restore CS0618 // Type or member is obsolete { private const string NewRepositoryUrl = "https://repo.jellyfin.org/files/plugin/manifest.json"; private const string OldRepositoryUrl = "https://repo.jellyfin.org/releases/plugin/manifest-stable.json"; @@ -22,15 +25,6 @@ public class UpdateDefaultPluginRepository : IMigrationRoutine _serverConfigurationManager = serverConfigurationManager; } - /// - public Guid Id => new("852816E0-2712-49A9-9240-C6FC5FCAD1A8"); - - /// - public string Name => "UpdateDefaultPluginRepository10.9"; - - /// - public bool PerformOnNewInstall => true; - /// public void Perform() { diff --git a/Jellyfin.Server/Migrations/Stages/CodeMigration.cs b/Jellyfin.Server/Migrations/Stages/CodeMigration.cs new file mode 100644 index 0000000000..1e4dfb237c --- /dev/null +++ b/Jellyfin.Server/Migrations/Stages/CodeMigration.cs @@ -0,0 +1,51 @@ +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace Jellyfin.Server.Migrations.Stages; + +internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute metadata) +{ + public Type MigrationType { get; } = migrationType; + + public JellyfinMigrationAttribute Metadata { get; } = metadata; + + public string BuildCodeMigrationId() + { + return Metadata.Order.ToString("yyyyMMddHHmmsss", CultureInfo.InvariantCulture) + "_" + MigrationType.Name!; + } + + public async Task Perform(IServiceProvider? serviceProvider, CancellationToken cancellationToken) + { +#pragma warning disable CS0618 // Type or member is obsolete + if (typeof(IMigrationRoutine).IsAssignableFrom(MigrationType)) + { + if (serviceProvider is null) + { + ((IMigrationRoutine)Activator.CreateInstance(MigrationType)!).Perform(); + } + else + { + ((IMigrationRoutine)ActivatorUtilities.CreateInstance(serviceProvider, MigrationType)).Perform(); +#pragma warning restore CS0618 // Type or member is obsolete + } + } + else if (typeof(IAsyncMigrationRoutine).IsAssignableFrom(MigrationType)) + { + if (serviceProvider is null) + { + await ((IAsyncMigrationRoutine)Activator.CreateInstance(MigrationType)!).PerformAsync(cancellationToken).ConfigureAwait(false); + } + else + { + await ((IAsyncMigrationRoutine)ActivatorUtilities.CreateInstance(serviceProvider, MigrationType)).PerformAsync(cancellationToken).ConfigureAwait(false); + } + } + else + { + throw new InvalidOperationException($"The type {MigrationType} does not implement either IMigrationRoutine or IAsyncMigrationRoutine and is not a valid migration type"); + } + } +} diff --git a/Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs b/Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs new file mode 100644 index 0000000000..d90ad3d9be --- /dev/null +++ b/Jellyfin.Server/Migrations/Stages/JellyfinMigrationStageTypes.cs @@ -0,0 +1,26 @@ +namespace Jellyfin.Server.Migrations.Stages; + +/// +/// Defines the stages the supports. +/// +#pragma warning disable CA1008 // Enums should have zero value +public enum JellyfinMigrationStageTypes +#pragma warning restore CA1008 // Enums should have zero value +{ + /// + /// Runs before services are initialised. + /// Reserved for migrations that are modifying the application server itself. Should be avoided if possible. + /// + PreInitialisation = 1, + + /// + /// Runs after the host has been configured and includes the database migrations. + /// Allows the mix order of migrations that contain application code and database changes. + /// + CoreInitialisaition = 2, + + /// + /// Runs after services has been registered and initialised. Last step before running the server. + /// + AppInitialisation = 3 +} diff --git a/Jellyfin.Server/Migrations/Stages/MigrationStage.cs b/Jellyfin.Server/Migrations/Stages/MigrationStage.cs new file mode 100644 index 0000000000..efcadbf006 --- /dev/null +++ b/Jellyfin.Server/Migrations/Stages/MigrationStage.cs @@ -0,0 +1,16 @@ +using System.Collections.ObjectModel; + +namespace Jellyfin.Server.Migrations.Stages; + +/// +/// Defines a Stage that can be Invoked and Handled at different times from the code. +/// +internal class MigrationStage : Collection +{ + public MigrationStage(JellyfinMigrationStageTypes stage) + { + Stage = stage; + } + + public JellyfinMigrationStageTypes Stage { get; } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 511306755b..12903544d3 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -9,17 +9,20 @@ using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; +using Emby.Server.Implementations.Configuration; +using Emby.Server.Implementations.Serialization; using Jellyfin.Database.Implementations; using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; +using Jellyfin.Server.Implementations.DatabaseConfiguration; +using Jellyfin.Server.Implementations.Extensions; using Jellyfin.Server.Implementations.StorageHelpers; +using Jellyfin.Server.Migrations; using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using Microsoft.AspNetCore.Hosting; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -126,7 +129,8 @@ namespace Jellyfin.Server StorageHelper.TestCommonPathsForStorageCapacity(appPaths, _loggerFactory.CreateLogger()); StartupHelpers.PerformStaticInitialization(); - await Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory).ConfigureAwait(false); + + await ApplyStartupMigrationAsync(appPaths, startupConfig).ConfigureAwait(false); do { @@ -171,9 +175,11 @@ namespace Jellyfin.Server // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. appHost.ServiceProvider = _jellyfinHost.Services; + await ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisaition).ConfigureAwait(false); await appHost.InitializeServices(startupConfig).ConfigureAwait(false); - await Migrations.MigrationRunner.Run(appHost, _loggerFactory).ConfigureAwait(false); + + await ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.AppInitialisation).ConfigureAwait(false); try { @@ -223,6 +229,45 @@ namespace Jellyfin.Server } } + /// + /// [Internal]Runs the startup Migrations. + /// + /// + /// Not intended to be used other then by jellyfin and its tests. + /// + /// Application Paths. + /// Startup Config. + /// A task. + public static async Task ApplyStartupMigrationAsync(ServerApplicationPaths appPaths, IConfiguration startupConfig) + { + var startupConfigurationManager = new ServerConfigurationManager(appPaths, _loggerFactory, new MyXmlSerializer()); + startupConfigurationManager.AddParts([new DatabaseConfigurationFactory()]); + var migrationStartupServiceProvider = new ServiceCollection() + .AddLogging(d => d.AddSerilog()) + .AddJellyfinDbContext(startupConfigurationManager, startupConfig) + .AddSingleton(appPaths) + .AddSingleton(appPaths); + var startupService = migrationStartupServiceProvider.BuildServiceProvider(); + var jellyfinMigrationService = ActivatorUtilities.CreateInstance(startupService); + await jellyfinMigrationService.CheckFirstTimeRunOrMigration(appPaths).ConfigureAwait(false); + await jellyfinMigrationService.MigrateStepAsync(Migrations.Stages.JellyfinMigrationStageTypes.PreInitialisation, startupService).ConfigureAwait(false); + } + + /// + /// [Internal]Runs the Jellyfin migrator service with the Core stage. + /// + /// + /// Not intended to be used other then by jellyfin and its tests. + /// + /// The service provider. + /// The stage to run. + /// A task. + public static async Task ApplyCoreMigrationsAsync(IServiceProvider serviceProvider, Migrations.Stages.JellyfinMigrationStageTypes jellyfinMigrationStage) + { + var jellyfinMigrationService = ActivatorUtilities.CreateInstance(serviceProvider); + await jellyfinMigrationService.MigrateStepAsync(jellyfinMigrationStage, serviceProvider).ConfigureAwait(false); + } + /// /// Create the application configuration. /// diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index ef1bf1769d..156d9618e1 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -76,6 +76,11 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider /// public async Task RunShutdownTask(CancellationToken cancellationToken) { + if (DbContextFactory is null) + { + return; + } + // Run before disposing the application var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs index a7fec2960c..c09bce52da 100644 --- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs @@ -6,6 +6,7 @@ using Emby.Server.Implementations; using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; using MediaBrowser.Common; +using MediaBrowser.Common.Configuration; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; @@ -103,7 +104,11 @@ namespace Jellyfin.Server.Integration.Tests var host = builder.Build(); var appHost = (TestAppHost)host.Services.GetRequiredService(); appHost.ServiceProvider = host.Services; + var applicationPaths = appHost.ServiceProvider.GetRequiredService(); + Program.ApplyStartupMigrationAsync((ServerApplicationPaths)applicationPaths, appHost.ServiceProvider.GetRequiredService()).GetAwaiter().GetResult(); + Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisaition).GetAwaiter().GetResult(); appHost.InitializeServices(Mock.Of()).GetAwaiter().GetResult(); + Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.AppInitialisation).GetAwaiter().GetResult(); host.Start(); appHost.RunStartupTasksAsync().GetAwaiter().GetResult(); From 5dc7962995ef73cecb8ef29fb628b49e9e6f5914 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 00:43:47 +0000 Subject: [PATCH 235/508] Update dependency Serilog.Sinks.File to v7 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ab9f1d81c2..d3c61bb524 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -63,7 +63,7 @@ - + From 0a0fcd3b19a079492f588141c21d994cf79e7542 Mon Sep 17 00:00:00 2001 From: besart-hajdari Date: Mon, 28 Apr 2025 04:51:20 -0400 Subject: [PATCH 236/508] Translated using Weblate (Albanian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sq/ --- .../Localization/Core/sq.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json index 91ed110425..263459289d 100644 --- a/Emby.Server.Implementations/Localization/Core/sq.json +++ b/Emby.Server.Implementations/Localization/Core/sq.json @@ -125,5 +125,15 @@ "External": "Jashtem", "HearingImpaired": "Dëgjimi i dëmtuar", "TaskRefreshTrickplayImages": "Krijo Imazhe Trickplay", - "TaskRefreshTrickplayImagesDescription": "Krijon pamje paraprake për video në bibliotekat e aktivizuara." + "TaskRefreshTrickplayImagesDescription": "Krijon pamje paraprake për video në bibliotekat e aktivizuara.", + "TaskExtractMediaSegments": "Skanim i segmenteve të medias", + "TaskExtractMediaSegmentsDescription": "Nxjerr ose merr segmente mediaje nga shtojcat që kanë të aktivizuar MediaSegment.", + "TaskMoveTrickplayImages": "Migron vendndodhjen e imazheve Trickplay", + "TaskMoveTrickplayImagesDescription": "Zhvendos skedarët ekzistues të trickplay sipas cilësimeve të bibliotekës.", + "TaskDownloadMissingLyrics": "Shkarko tekstet e këngëve që mungojnë", + "TaskDownloadMissingLyricsDescription": "Shkarkon tekstet e këngëve", + "TaskCleanCollectionsAndPlaylists": "Pastron koleksionet dhe listat e këngëve", + "TaskCleanCollectionsAndPlaylistsDescription": "Heq elementet nga koleksionet dhe listat e këngëve që nuk ekzistojnë më.", + "TaskAudioNormalization": "Normalizimi i audios", + "TaskAudioNormalizationDescription": "Skannon skedarët për të dhëna të normalizimit të audios." } From 5c9f70c3752bd7297cb85bdc7ce748363a16ad8b Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 30 Apr 2025 09:29:13 +0200 Subject: [PATCH 237/508] Cleanup Tasks and Validators --- .../Library/Validators/ArtistsPostScanTask.cs | 69 +- .../Library/Validators/ArtistsValidator.cs | 163 ++- .../Validators/CollectionPostScanTask.cs | 259 ++-- .../Library/Validators/GenresPostScanTask.cs | 69 +- .../Library/Validators/GenresValidator.cs | 157 ++- .../Validators/MusicGenresPostScanTask.cs | 69 +- .../Validators/MusicGenresValidator.cs | 119 +- .../Library/Validators/PeopleValidator.cs | 187 ++- .../Library/Validators/StudiosPostScanTask.cs | 71 +- .../Library/Validators/StudiosValidator.cs | 157 ++- .../ScheduledTasks/ScheduledTaskWorker.cs | 1231 ++++++++--------- .../ScheduledTasks/TaskManager.cs | 387 +++--- .../Tasks/AudioNormalizationTask.cs | 15 +- .../ScheduledTasks/Tasks/ChapterImagesTask.cs | 236 ++-- .../Tasks/CleanActivityLogTask.cs | 113 +- .../CleanupCollectionAndPlaylistPathsTask.cs | 11 +- .../Tasks/DeleteCacheFileTask.cs | 215 ++- .../ScheduledTasks/Tasks/DeleteLogFileTask.cs | 158 +-- .../Tasks/DeleteTranscodeFileTask.cs | 205 ++- .../Tasks/MediaSegmentExtractionTask.cs | 4 +- .../Tasks/OptimizeDatabaseTask.cs | 130 +- .../Tasks/PeopleValidationTask.cs | 108 +- .../ScheduledTasks/Tasks/PluginUpdateTask.cs | 178 +-- .../Tasks/RefreshMediaLibraryTask.cs | 91 +- .../ScheduledTasks/Triggers/DailyTrigger.cs | 141 +- .../Triggers/IntervalTrigger.cs | 169 ++- .../ScheduledTasks/Triggers/StartupTrigger.cs | 77 +- .../ScheduledTasks/Triggers/WeeklyTrigger.cs | 173 ++- 28 files changed, 2460 insertions(+), 2502 deletions(-) diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs index d51f9aaa79..a31d5eccac 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs @@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class ArtistsPostScanTask. +/// +public class ArtistsPostScanTask : ILibraryPostScanTask { /// - /// Class ArtistsPostScanTask. + /// The _library manager. /// - public class ArtistsPostScanTask : ILibraryPostScanTask + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public ArtistsPostScanTask( + ILibraryManager libraryManager, + ILogger logger, + IItemRepository itemRepo) { - /// - /// The _library manager. - /// - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IItemRepository _itemRepo; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public ArtistsPostScanTask( - ILibraryManager libraryManager, - ILogger logger, - IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public Task Run(IProgress progress, CancellationToken cancellationToken) - { - return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); - } + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); } } diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 7591e8391f..7cc851b73b 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -10,102 +10,101 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class ArtistsValidator. +/// +public class ArtistsValidator { /// - /// Class ArtistsValidator. + /// The library manager. /// - public class ArtistsValidator + private readonly ILibraryManager _libraryManager; + + /// + /// The logger. + /// + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public ArtistsValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) { - /// - /// The library manager. - /// - private readonly ILibraryManager _libraryManager; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// - /// The logger. - /// - private readonly ILogger _logger; - private readonly IItemRepository _itemRepo; + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetAllArtistNames(); - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public ArtistsValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + var numComplete = 0; + var count = names.Count; + + foreach (var name in names) { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public async Task Run(IProgress progress, CancellationToken cancellationToken) - { - var names = _itemRepo.GetAllArtistNames(); - - var numComplete = 0; - var count = names.Count; - - foreach (var name in names) + try { - try - { - var item = _libraryManager.GetArtist(name); + var item = _libraryManager.GetArtist(name); - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Don't clutter the log - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {ArtistName}", name); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - - progress.Report(percent); + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {ArtistName}", name); } - var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); + } + + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.MusicArtist], + IsDeadArtist = true, + IsLocked = false + }).Cast().ToList(); + + foreach (var item in deadEntities) + { + if (!item.IsAccessedByName) { - IncludeItemTypes = new[] { BaseItemKind.MusicArtist }, - IsDeadArtist = true, - IsLocked = false - }).Cast().ToList(); - - foreach (var item in deadEntities) - { - if (!item.IsAccessedByName) - { - continue; - } - - _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); - - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); + continue; } - progress.Report(100); + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs index 337b1afdd4..38631e0de8 100644 --- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs @@ -9,149 +9,146 @@ using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class CollectionPostScanTask. +/// +public class CollectionPostScanTask : ILibraryPostScanTask { + private readonly ILibraryManager _libraryManager; + private readonly ICollectionManager _collectionManager; + private readonly ILogger _logger; + /// - /// Class CollectionPostScanTask. + /// Initializes a new instance of the class. /// - public class CollectionPostScanTask : ILibraryPostScanTask + /// The library manager. + /// The collection manager. + /// The logger. + public CollectionPostScanTask( + ILibraryManager libraryManager, + ICollectionManager collectionManager, + ILogger logger) { - private readonly ILibraryManager _libraryManager; - private readonly ICollectionManager _collectionManager; - private readonly ILogger _logger; + _libraryManager = libraryManager; + _collectionManager = collectionManager; + _logger = logger; + } - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The collection manager. - /// The logger. - public CollectionPostScanTask( - ILibraryManager libraryManager, - ICollectionManager collectionManager, - ILogger logger) + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var collectionNameMoviesMap = new Dictionary>(); + + foreach (var library in _libraryManager.RootFolder.Children) { - _libraryManager = libraryManager; - _collectionManager = collectionManager; - _logger = logger; + if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection) + { + continue; + } + + var startIndex = 0; + var pagesize = 1000; + + while (true) + { + var movies = _libraryManager.GetItemList(new InternalItemsQuery + { + MediaTypes = [MediaType.Video], + IncludeItemTypes = [BaseItemKind.Movie], + IsVirtualItem = false, + OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)], + Parent = library, + StartIndex = startIndex, + Limit = pagesize, + Recursive = true + }); + + foreach (var m in movies) + { + if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName)) + { + if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList)) + { + movieList.Add(movie.Id); + } + else + { + collectionNameMoviesMap[movie.CollectionName] = new HashSet { movie.Id }; + } + } + } + + if (movies.Count < pagesize) + { + break; + } + + startIndex += pagesize; + } } - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public async Task Run(IProgress progress, CancellationToken cancellationToken) + var numComplete = 0; + var count = collectionNameMoviesMap.Count; + + if (count == 0) { - var collectionNameMoviesMap = new Dictionary>(); - - foreach (var library in _libraryManager.RootFolder.Children) - { - if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection) - { - continue; - } - - var startIndex = 0; - var pagesize = 1000; - - while (true) - { - var movies = _libraryManager.GetItemList(new InternalItemsQuery - { - MediaTypes = new[] { MediaType.Video }, - IncludeItemTypes = new[] { BaseItemKind.Movie }, - IsVirtualItem = false, - OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, - Parent = library, - StartIndex = startIndex, - Limit = pagesize, - Recursive = true - }); - - foreach (var m in movies) - { - if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName)) - { - if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList)) - { - movieList.Add(movie.Id); - } - else - { - collectionNameMoviesMap[movie.CollectionName] = new HashSet { movie.Id }; - } - } - } - - if (movies.Count < pagesize) - { - break; - } - - startIndex += pagesize; - } - } - - var numComplete = 0; - var count = collectionNameMoviesMap.Count; - - if (count == 0) - { - progress.Report(100); - return; - } - - var boxSets = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.BoxSet }, - CollapseBoxSetItems = false, - Recursive = true - }); - - foreach (var (collectionName, movieIds) in collectionNameMoviesMap) - { - try - { - var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet; - if (boxSet is null) - { - // won't automatically create collection if only one movie in it - if (movieIds.Count >= 2) - { - boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions - { - Name = collectionName, - IsLocked = true - }); - - await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds); - } - } - else - { - await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - - progress.Report(percent); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds); - } - } - progress.Report(100); + return; } + + var boxSets = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.BoxSet], + CollapseBoxSetItems = false, + Recursive = true + }); + + foreach (var (collectionName, movieIds) in collectionNameMoviesMap) + { + try + { + var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet; + if (boxSet is null) + { + // won't automatically create collection if only one movie in it + if (movieIds.Count >= 2) + { + boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions + { + Name = collectionName, + IsLocked = true + }).ConfigureAwait(false); + + await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false); + } + } + else + { + await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false); + } + + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds); + } + } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs index d21d2887b0..5097e0073d 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs @@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class GenresPostScanTask. +/// +public class GenresPostScanTask : ILibraryPostScanTask { /// - /// Class GenresPostScanTask. + /// The _library manager. /// - public class GenresPostScanTask : ILibraryPostScanTask + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public GenresPostScanTask( + ILibraryManager libraryManager, + ILogger logger, + IItemRepository itemRepo) { - /// - /// The _library manager. - /// - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IItemRepository _itemRepo; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public GenresPostScanTask( - ILibraryManager libraryManager, - ILogger logger, - IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public Task Run(IProgress progress, CancellationToken cancellationToken) - { - return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); - } + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); } } diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs index 364770fcdc..fbfc9f7d54 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs @@ -8,97 +8,96 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class GenresValidator. +/// +public class GenresValidator { /// - /// Class GenresValidator. + /// The library manager. /// - public class GenresValidator + private readonly ILibraryManager _libraryManager; + private readonly IItemRepository _itemRepo; + + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public GenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) { - /// - /// The library manager. - /// - private readonly ILibraryManager _libraryManager; - private readonly IItemRepository _itemRepo; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// - /// The logger. - /// - private readonly ILogger _logger; + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetGenreNames(); - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public GenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + var numComplete = 0; + var count = names.Count; + + foreach (var name in names) { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public async Task Run(IProgress progress, CancellationToken cancellationToken) - { - var names = _itemRepo.GetGenreNames(); - - var numComplete = 0; - var count = names.Count; - - foreach (var name in names) + try { - try - { - var item = _libraryManager.GetGenre(name); + var item = _libraryManager.GetGenre(name); - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Don't clutter the log - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {GenreName}", name); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - - progress.Report(percent); + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {GenreName}", name); } - var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre], - IsDeadGenre = true, - IsLocked = false - }); + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; - foreach (var item in deadEntities) - { - _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); - - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); - } - - progress.Report(100); + progress.Report(percent); } + + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre], + IsDeadGenre = true, + IsLocked = false + }); + + foreach (var item in deadEntities) + { + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); + } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs index be119866b1..76658a81b5 100644 --- a/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs @@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class MusicGenresPostScanTask. +/// +public class MusicGenresPostScanTask : ILibraryPostScanTask { /// - /// Class MusicGenresPostScanTask. + /// The library manager. /// - public class MusicGenresPostScanTask : ILibraryPostScanTask + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public MusicGenresPostScanTask( + ILibraryManager libraryManager, + ILogger logger, + IItemRepository itemRepo) { - /// - /// The library manager. - /// - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IItemRepository _itemRepo; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public MusicGenresPostScanTask( - ILibraryManager libraryManager, - ILogger logger, - IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public Task Run(IProgress progress, CancellationToken cancellationToken) - { - return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); - } + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); } } diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs index 1ecf4c87c9..6203bce2bc 100644 --- a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs @@ -5,77 +5,76 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class MusicGenresValidator. +/// +public class MusicGenresValidator { /// - /// Class MusicGenresValidator. + /// The library manager. /// - public class MusicGenresValidator + private readonly ILibraryManager _libraryManager; + + /// + /// The logger. + /// + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public MusicGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) { - /// - /// The library manager. - /// - private readonly ILibraryManager _libraryManager; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - /// - /// The logger. - /// - private readonly ILogger _logger; - private readonly IItemRepository _itemRepo; + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetMusicGenreNames(); - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public MusicGenresValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + var numComplete = 0; + var count = names.Count; + + foreach (var name in names) { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public async Task Run(IProgress progress, CancellationToken cancellationToken) - { - var names = _itemRepo.GetMusicGenreNames(); - - var numComplete = 0; - var count = names.Count; - - foreach (var name in names) + try { - try - { - var item = _libraryManager.GetMusicGenre(name); + var item = _libraryManager.GetMusicGenre(name); - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Don't clutter the log - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {GenreName}", name); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - - progress.Report(percent); + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {GenreName}", name); } - progress.Report(100); + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; + + progress.Report(percent); } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index 725b8f76c7..b7fd24fa5c 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -9,119 +9,114 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class PeopleValidator. +/// +public class PeopleValidator { /// - /// Class PeopleValidator. + /// The _library manager. /// - public class PeopleValidator + private readonly ILibraryManager _libraryManager; + + /// + /// The _logger. + /// + private readonly ILogger _logger; + + private readonly IFileSystem _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The file system. + public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem) { - /// - /// The _library manager. - /// - private readonly ILibraryManager _libraryManager; + _libraryManager = libraryManager; + _logger = logger; + _fileSystem = fileSystem; + } - /// - /// The _logger. - /// - private readonly ILogger _logger; + /// + /// Validates the people. + /// + /// The cancellation token. + /// The progress. + /// Task. + public async Task ValidatePeople(CancellationToken cancellationToken, IProgress progress) + { + var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery()); - private readonly IFileSystem _fileSystem; + var numComplete = 0; - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The file system. - public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem) + var numPeople = people.Count; + + _logger.LogDebug("Will refresh {Amount} people", numPeople); + + foreach (var person in people) { - _libraryManager = libraryManager; - _logger = logger; - _fileSystem = fileSystem; - } + cancellationToken.ThrowIfCancellationRequested(); - /// - /// Validates the people. - /// - /// The cancellation token. - /// The progress. - /// Task. - public async Task ValidatePeople(CancellationToken cancellationToken, IProgress progress) - { - var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery()); - - var numComplete = 0; - - var numPeople = people.Count; - - _logger.LogDebug("Will refresh {0} people", numPeople); - - foreach (var person in people) + try { - cancellationToken.ThrowIfCancellationRequested(); - - try + var item = _libraryManager.GetPerson(person); + if (item is null) { - var item = _libraryManager.GetPerson(person); - if (item is null) - { - _logger.LogWarning("Failed to get person: {Name}", person); - continue; - } - - var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - ImageRefreshMode = MetadataRefreshMode.ValidationOnly, - MetadataRefreshMode = MetadataRefreshMode.ValidationOnly - }; - - await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error validating IBN entry {Person}", person); + _logger.LogWarning("Failed to get person: {Name}", person); + continue; } - // Update progress - numComplete++; - double percent = numComplete; - percent /= numPeople; + var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ImageRefreshMode = MetadataRefreshMode.ValidationOnly, + MetadataRefreshMode = MetadataRefreshMode.ValidationOnly + }; - progress.Report(100 * percent); + await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error validating IBN entry {Person}", person); } - var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = [BaseItemKind.Person], - IsDeadPerson = true, - IsLocked = false - }); + // Update progress + numComplete++; + double percent = numComplete; + percent /= numPeople; - foreach (var item in deadEntities) - { - _logger.LogInformation( - "Deleting dead {2} {0} {1}.", - item.Id.ToString("N", CultureInfo.InvariantCulture), - item.Name, - item.GetType().Name); - - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); - } - - progress.Report(100); - - _logger.LogInformation("People validation complete"); + progress.Report(100 * percent); } + + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.Person], + IsDeadPerson = true, + IsLocked = false + }); + + foreach (var item in deadEntities) + { + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); + } + + progress.Report(100); + + _logger.LogInformation("People validation complete"); } } diff --git a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs index c682b156b8..67c56c104d 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs @@ -5,46 +5,45 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class MusicGenresPostScanTask. +/// +public class StudiosPostScanTask : ILibraryPostScanTask { /// - /// Class MusicGenresPostScanTask. + /// The _library manager. /// - public class StudiosPostScanTask : ILibraryPostScanTask + private readonly ILibraryManager _libraryManager; + + private readonly ILogger _logger; + private readonly IItemRepository _itemRepo; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public StudiosPostScanTask( + ILibraryManager libraryManager, + ILogger logger, + IItemRepository itemRepo) { - /// - /// The _library manager. - /// - private readonly ILibraryManager _libraryManager; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - private readonly ILogger _logger; - private readonly IItemRepository _itemRepo; - - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public StudiosPostScanTask( - ILibraryManager libraryManager, - ILogger logger, - IItemRepository itemRepo) - { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public Task Run(IProgress progress, CancellationToken cancellationToken) - { - return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); - } + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public Task Run(IProgress progress, CancellationToken cancellationToken) + { + return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken); } } diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index 26bc49c1f0..5b87e4d9d0 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -8,98 +8,97 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Library.Validators +namespace Emby.Server.Implementations.Library.Validators; + +/// +/// Class StudiosValidator. +/// +public class StudiosValidator { /// - /// Class StudiosValidator. + /// The library manager. /// - public class StudiosValidator + private readonly ILibraryManager _libraryManager; + + private readonly IItemRepository _itemRepo; + + /// + /// The logger. + /// + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The logger. + /// The item repository. + public StudiosValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) { - /// - /// The library manager. - /// - private readonly ILibraryManager _libraryManager; + _libraryManager = libraryManager; + _logger = logger; + _itemRepo = itemRepo; + } - private readonly IItemRepository _itemRepo; + /// + /// Runs the specified progress. + /// + /// The progress. + /// The cancellation token. + /// Task. + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + var names = _itemRepo.GetStudioNames(); - /// - /// The logger. - /// - private readonly ILogger _logger; + var numComplete = 0; + var count = names.Count; - /// - /// Initializes a new instance of the class. - /// - /// The library manager. - /// The logger. - /// The item repository. - public StudiosValidator(ILibraryManager libraryManager, ILogger logger, IItemRepository itemRepo) + foreach (var name in names) { - _libraryManager = libraryManager; - _logger = logger; - _itemRepo = itemRepo; - } - - /// - /// Runs the specified progress. - /// - /// The progress. - /// The cancellation token. - /// Task. - public async Task Run(IProgress progress, CancellationToken cancellationToken) - { - var names = _itemRepo.GetStudioNames(); - - var numComplete = 0; - var count = names.Count; - - foreach (var name in names) + try { - try - { - var item = _libraryManager.GetStudio(name); + var item = _libraryManager.GetStudio(name); - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Don't clutter the log - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error refreshing {StudioName}", name); - } - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - - progress.Report(percent); + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Don't clutter the log + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing {StudioName}", name); } - var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.Studio }, - IsDeadStudio = true, - IsLocked = false - }); + numComplete++; + double percent = numComplete; + percent /= count; + percent *= 100; - foreach (var item in deadEntities) - { - _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); - - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); - } - - progress.Report(100); + progress.Report(percent); } + + var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.Studio], + IsDeadStudio = true, + IsLocked = false + }); + + foreach (var item in deadEntities) + { + _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); + } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 985f0a8f85..24f554981a 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -16,663 +16,662 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks +namespace Emby.Server.Implementations.ScheduledTasks; + +/// +/// Class ScheduledTaskWorker. +/// +public class ScheduledTaskWorker : IScheduledTaskWorker { + private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private readonly IApplicationPaths _applicationPaths; + private readonly ILogger _logger; + private readonly ITaskManager _taskManager; + private readonly Lock _lastExecutionResultSyncLock = new(); + private bool _readFromFile; + private TaskResult _lastExecutionResult; + private Task _currentTask; + private Tuple[] _triggers; + private string _id; + /// - /// Class ScheduledTaskWorker. + /// Initializes a new instance of the class. /// - public class ScheduledTaskWorker : IScheduledTaskWorker + /// The scheduled task. + /// The application paths. + /// The task manager. + /// The logger. + /// + /// scheduledTask + /// or + /// applicationPaths + /// or + /// taskManager + /// or + /// jsonSerializer + /// or + /// logger. + /// + public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger) { - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private readonly IApplicationPaths _applicationPaths; - private readonly ILogger _logger; - private readonly ITaskManager _taskManager; - private readonly Lock _lastExecutionResultSyncLock = new(); - private bool _readFromFile; - private TaskResult _lastExecutionResult; - private Task _currentTask; - private Tuple[] _triggers; - private string _id; + ArgumentNullException.ThrowIfNull(scheduledTask); + ArgumentNullException.ThrowIfNull(applicationPaths); + ArgumentNullException.ThrowIfNull(taskManager); + ArgumentNullException.ThrowIfNull(logger); - /// - /// Initializes a new instance of the class. - /// - /// The scheduled task. - /// The application paths. - /// The task manager. - /// The logger. - /// - /// scheduledTask - /// or - /// applicationPaths - /// or - /// taskManager - /// or - /// jsonSerializer - /// or - /// logger. - /// - public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger) + ScheduledTask = scheduledTask; + _applicationPaths = applicationPaths; + _taskManager = taskManager; + _logger = logger; + + InitTriggerEvents(); + } + + /// + public event EventHandler> TaskProgress; + + /// + public IScheduledTask ScheduledTask { get; private set; } + + /// + public TaskResult LastExecutionResult + { + get { - ArgumentNullException.ThrowIfNull(scheduledTask); - ArgumentNullException.ThrowIfNull(applicationPaths); - ArgumentNullException.ThrowIfNull(taskManager); - ArgumentNullException.ThrowIfNull(logger); + var path = GetHistoryFilePath(); - ScheduledTask = scheduledTask; - _applicationPaths = applicationPaths; - _taskManager = taskManager; - _logger = logger; - - InitTriggerEvents(); - } - - /// - public event EventHandler> TaskProgress; - - /// - public IScheduledTask ScheduledTask { get; private set; } - - /// - public TaskResult LastExecutionResult - { - get + lock (_lastExecutionResultSyncLock) { - var path = GetHistoryFilePath(); - - lock (_lastExecutionResultSyncLock) + if (_lastExecutionResult is null && !_readFromFile) { - if (_lastExecutionResult is null && !_readFromFile) + if (File.Exists(path)) { - if (File.Exists(path)) + var bytes = File.ReadAllBytes(path); + if (bytes.Length > 0) { - var bytes = File.ReadAllBytes(path); - if (bytes.Length > 0) + try { - try - { - _lastExecutionResult = JsonSerializer.Deserialize(bytes, _jsonOptions); - } - catch (JsonException ex) - { - _logger.LogError(ex, "Error deserializing {File}", path); - } + _lastExecutionResult = JsonSerializer.Deserialize(bytes, _jsonOptions); } - else + catch (JsonException ex) { - _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.", path); + _logger.LogError(ex, "Error deserializing {File}", path); } } - - _readFromFile = true; - } - } - - return _lastExecutionResult; - } - - private set - { - _lastExecutionResult = value; - - var path = GetHistoryFilePath(); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_lastExecutionResultSyncLock) - { - using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); - using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream); - JsonSerializer.Serialize(jsonStream, value, _jsonOptions); - } - } - } - - /// - public string Name => ScheduledTask.Name; - - /// - public string Description => ScheduledTask.Description; - - /// - public string Category => ScheduledTask.Category; - - /// - /// Gets or sets the current cancellation token. - /// - /// The current cancellation token source. - private CancellationTokenSource CurrentCancellationTokenSource { get; set; } - - /// - /// Gets or sets the current execution start time. - /// - /// The current execution start time. - private DateTime CurrentExecutionStartTime { get; set; } - - /// - public TaskState State - { - get - { - if (CurrentCancellationTokenSource is not null) - { - return CurrentCancellationTokenSource.IsCancellationRequested - ? TaskState.Cancelling - : TaskState.Running; - } - - return TaskState.Idle; - } - } - - /// - public double? CurrentProgress { get; private set; } - - /// - /// Gets or sets the triggers that define when the task will run. - /// - /// The triggers. - private Tuple[] InternalTriggers - { - get => _triggers; - set - { - ArgumentNullException.ThrowIfNull(value); - - // Cleanup current triggers - if (_triggers is not null) - { - DisposeTriggers(); - } - - _triggers = value.ToArray(); - - ReloadTriggerEvents(false); - } - } - - /// - public IReadOnlyList Triggers - { - get - { - return Array.ConvertAll(InternalTriggers, i => i.Item1); - } - - set - { - ArgumentNullException.ThrowIfNull(value); - - // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly - var triggerList = value.Where(i => i is not null).ToArray(); - - SaveTriggers(triggerList); - - InternalTriggers = Array.ConvertAll(triggerList, i => new Tuple(i, GetTrigger(i))); - } - } - - /// - public string Id - { - get - { - return _id ??= ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture); - } - } - - private void InitTriggerEvents() - { - _triggers = LoadTriggers(); - ReloadTriggerEvents(true); - } - - /// - public void ReloadTriggerEvents() - { - ReloadTriggerEvents(false); - } - - /// - /// Reloads the trigger events. - /// - /// if set to true [is application startup]. - private void ReloadTriggerEvents(bool isApplicationStartup) - { - foreach (var triggerInfo in InternalTriggers) - { - var trigger = triggerInfo.Item2; - - trigger.Stop(); - - trigger.Triggered -= OnTriggerTriggered; - trigger.Triggered += OnTriggerTriggered; - trigger.Start(LastExecutionResult, _logger, Name, isApplicationStartup); - } - } - - /// - /// Handles the Triggered event of the trigger control. - /// - /// The source of the event. - /// The instance containing the event data. - private async void OnTriggerTriggered(object sender, EventArgs e) - { - var trigger = (ITaskTrigger)sender; - - if (ScheduledTask is IConfigurableScheduledTask configurableTask && !configurableTask.IsEnabled) - { - return; - } - - _logger.LogDebug("{0} fired for task: {1}", trigger.GetType().Name, Name); - - trigger.Stop(); - - _taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions); - - await Task.Delay(1000).ConfigureAwait(false); - - trigger.Start(LastExecutionResult, _logger, Name, false); - } - - /// - /// Executes the task. - /// - /// Task options. - /// Task. - /// Cannot execute a Task that is already running. - public async Task Execute(TaskOptions options) - { - var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false)); - - _currentTask = task; - - try - { - await task.ConfigureAwait(false); - } - finally - { - _currentTask = null; - GC.Collect(); - } - } - - private async Task ExecuteInternal(TaskOptions options) - { - // Cancel the current execution, if any - if (CurrentCancellationTokenSource is not null) - { - throw new InvalidOperationException("Cannot execute a Task that is already running"); - } - - var progress = new Progress(); - - CurrentCancellationTokenSource = new CancellationTokenSource(); - - _logger.LogDebug("Executing {0}", Name); - - ((TaskManager)_taskManager).OnTaskExecuting(this); - - progress.ProgressChanged += OnProgressChanged; - - TaskCompletionStatus status; - CurrentExecutionStartTime = DateTime.UtcNow; - - Exception failureException = null; - - try - { - if (options is not null && options.MaxRuntimeTicks.HasValue) - { - CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value)); - } - - await ScheduledTask.ExecuteAsync(progress, CurrentCancellationTokenSource.Token).ConfigureAwait(false); - - status = TaskCompletionStatus.Completed; - } - catch (OperationCanceledException) - { - status = TaskCompletionStatus.Cancelled; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error executing Scheduled Task"); - - failureException = ex; - - status = TaskCompletionStatus.Failed; - } - - var startTime = CurrentExecutionStartTime; - var endTime = DateTime.UtcNow; - - progress.ProgressChanged -= OnProgressChanged; - CurrentCancellationTokenSource.Dispose(); - CurrentCancellationTokenSource = null; - CurrentProgress = null; - - OnTaskCompleted(startTime, endTime, status, failureException); - } - - /// - /// Progress_s the progress changed. - /// - /// The sender. - /// The e. - private void OnProgressChanged(object sender, double e) - { - e = Math.Min(e, 100); - - CurrentProgress = e; - - TaskProgress?.Invoke(this, new GenericEventArgs(e)); - } - - /// - /// Stops the task if it is currently executing. - /// - /// Cannot cancel a Task unless it is in the Running state. - public void Cancel() - { - if (State != TaskState.Running) - { - throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state."); - } - - CancelIfRunning(); - } - - /// - /// Cancels if running. - /// - public void CancelIfRunning() - { - if (State == TaskState.Running) - { - _logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name); - CurrentCancellationTokenSource.Cancel(); - } - } - - /// - /// Gets the scheduled tasks configuration directory. - /// - /// System.String. - private string GetScheduledTasksConfigurationDirectory() - { - return Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); - } - - /// - /// Gets the scheduled tasks data directory. - /// - /// System.String. - private string GetScheduledTasksDataDirectory() - { - return Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"); - } - - /// - /// Gets the history file path. - /// - /// The history file path. - private string GetHistoryFilePath() - { - return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js"); - } - - /// - /// Gets the configuration file path. - /// - /// System.String. - private string GetConfigurationFilePath() - { - return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js"); - } - - /// - /// Loads the triggers. - /// - /// IEnumerable{BaseTaskTrigger}. - private Tuple[] LoadTriggers() - { - // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly - var settings = LoadTriggerSettings().Where(i => i is not null); - - return settings.Select(i => new Tuple(i, GetTrigger(i))).ToArray(); - } - - private TaskTriggerInfo[] LoadTriggerSettings() - { - string path = GetConfigurationFilePath(); - TaskTriggerInfo[] list = null; - if (File.Exists(path)) - { - var bytes = File.ReadAllBytes(path); - list = JsonSerializer.Deserialize(bytes, _jsonOptions); - } - - // Return defaults if file doesn't exist. - return list ?? GetDefaultTriggers(); - } - - private TaskTriggerInfo[] GetDefaultTriggers() - { - try - { - return ScheduledTask.GetDefaultTriggers().ToArray(); - } - catch - { - return - [ - new() - { - IntervalTicks = TimeSpan.FromDays(1).Ticks, - Type = TaskTriggerInfoType.IntervalTrigger - } - ]; - } - } - - /// - /// Saves the triggers. - /// - /// The triggers. - private void SaveTriggers(TaskTriggerInfo[] triggers) - { - var path = GetConfigurationFilePath(); - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); - using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream); - JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions); - } - - /// - /// Called when [task completed]. - /// - /// The start time. - /// The end time. - /// The status. - /// The exception. - private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex) - { - var elapsedTime = endTime - startTime; - - _logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds); - - var result = new TaskResult - { - StartTimeUtc = startTime, - EndTimeUtc = endTime, - Status = status, - Name = Name, - Id = Id - }; - - result.Key = ScheduledTask.Key; - - if (ex is not null) - { - result.ErrorMessage = ex.Message; - result.LongErrorMessage = ex.StackTrace; - } - - LastExecutionResult = result; - - ((TaskManager)_taskManager).OnTaskCompleted(this, result); - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - DisposeTriggers(); - - var wasRunning = State == TaskState.Running; - var startTime = CurrentExecutionStartTime; - - var token = CurrentCancellationTokenSource; - if (token is not null) - { - try - { - _logger.LogInformation("{Name}: Cancelling", Name); - token.Cancel(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error calling CancellationToken.Cancel();"); - } - } - - var task = _currentTask; - if (task is not null) - { - try - { - _logger.LogInformation("{Name}: Waiting on Task", Name); - var exited = task.Wait(2000); - - if (exited) - { - _logger.LogInformation("{Name}: Task exited", Name); - } else { - _logger.LogInformation("{Name}: Timed out waiting for task to stop", Name); + _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.", path); } } - catch (Exception ex) - { - _logger.LogError(ex, "Error calling Task.WaitAll();"); - } - } - if (token is not null) - { - try - { - _logger.LogDebug("{Name}: Disposing CancellationToken", Name); - token.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error calling CancellationToken.Dispose();"); - } - } - - if (wasRunning) - { - OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null); + _readFromFile = true; } } + + return _lastExecutionResult; } - /// - /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger. - /// - /// The info. - /// BaseTaskTrigger. - /// Invalid trigger type: + info.Type. - private ITaskTrigger GetTrigger(TaskTriggerInfo info) + private set { - var options = new TaskOptions - { - MaxRuntimeTicks = info.MaxRuntimeTicks - }; + _lastExecutionResult = value; - if (info.Type == TaskTriggerInfoType.DailyTrigger) - { - if (!info.TimeOfDayTicks.HasValue) - { - throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info)); - } + var path = GetHistoryFilePath(); + Directory.CreateDirectory(Path.GetDirectoryName(path)); - return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options); + lock (_lastExecutionResultSyncLock) + { + using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream); + JsonSerializer.Serialize(jsonStream, value, _jsonOptions); + } + } + } + + /// + public string Name => ScheduledTask.Name; + + /// + public string Description => ScheduledTask.Description; + + /// + public string Category => ScheduledTask.Category; + + /// + /// Gets or sets the current cancellation token. + /// + /// The current cancellation token source. + private CancellationTokenSource CurrentCancellationTokenSource { get; set; } + + /// + /// Gets or sets the current execution start time. + /// + /// The current execution start time. + private DateTime CurrentExecutionStartTime { get; set; } + + /// + public TaskState State + { + get + { + if (CurrentCancellationTokenSource is not null) + { + return CurrentCancellationTokenSource.IsCancellationRequested + ? TaskState.Cancelling + : TaskState.Running; } - if (info.Type == TaskTriggerInfoType.WeeklyTrigger) + return TaskState.Idle; + } + } + + /// + public double? CurrentProgress { get; private set; } + + /// + /// Gets or sets the triggers that define when the task will run. + /// + /// The triggers. + private Tuple[] InternalTriggers + { + get => _triggers; + set + { + ArgumentNullException.ThrowIfNull(value); + + // Cleanup current triggers + if (_triggers is not null) { - if (!info.TimeOfDayTicks.HasValue) - { - throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info)); - } - - if (!info.DayOfWeek.HasValue) - { - throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info)); - } - - return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options); + DisposeTriggers(); } - if (info.Type == TaskTriggerInfoType.IntervalTrigger) - { - if (!info.IntervalTicks.HasValue) - { - throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info)); - } + _triggers = value.ToArray(); - return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options); - } + ReloadTriggerEvents(false); + } + } - if (info.Type == TaskTriggerInfoType.StartupTrigger) - { - return new StartupTrigger(options); - } - - throw new ArgumentException("Unrecognized trigger type: " + info.Type); + /// + public IReadOnlyList Triggers + { + get + { + return Array.ConvertAll(InternalTriggers, i => i.Item1); } - /// - /// Disposes each trigger. - /// - private void DisposeTriggers() + set { - foreach (var triggerInfo in InternalTriggers) + ArgumentNullException.ThrowIfNull(value); + + // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly + var triggerList = value.Where(i => i is not null).ToArray(); + + SaveTriggers(triggerList); + + InternalTriggers = Array.ConvertAll(triggerList, i => new Tuple(i, GetTrigger(i))); + } + } + + /// + public string Id + { + get + { + return _id ??= ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture); + } + } + + private void InitTriggerEvents() + { + _triggers = LoadTriggers(); + ReloadTriggerEvents(true); + } + + /// + public void ReloadTriggerEvents() + { + ReloadTriggerEvents(false); + } + + /// + /// Reloads the trigger events. + /// + /// if set to true [is application startup]. + private void ReloadTriggerEvents(bool isApplicationStartup) + { + foreach (var triggerInfo in InternalTriggers) + { + var trigger = triggerInfo.Item2; + + trigger.Stop(); + + trigger.Triggered -= OnTriggerTriggered; + trigger.Triggered += OnTriggerTriggered; + trigger.Start(LastExecutionResult, _logger, Name, isApplicationStartup); + } + } + + /// + /// Handles the Triggered event of the trigger control. + /// + /// The source of the event. + /// The instance containing the event data. + private async void OnTriggerTriggered(object sender, EventArgs e) + { + var trigger = (ITaskTrigger)sender; + + if (ScheduledTask is IConfigurableScheduledTask configurableTask && !configurableTask.IsEnabled) + { + return; + } + + _logger.LogDebug("{0} fired for task: {1}", trigger.GetType().Name, Name); + + trigger.Stop(); + + _taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions); + + await Task.Delay(1000).ConfigureAwait(false); + + trigger.Start(LastExecutionResult, _logger, Name, false); + } + + /// + /// Executes the task. + /// + /// Task options. + /// Task. + /// Cannot execute a Task that is already running. + public async Task Execute(TaskOptions options) + { + var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false)); + + _currentTask = task; + + try + { + await task.ConfigureAwait(false); + } + finally + { + _currentTask = null; + GC.Collect(); + } + } + + private async Task ExecuteInternal(TaskOptions options) + { + // Cancel the current execution, if any + if (CurrentCancellationTokenSource is not null) + { + throw new InvalidOperationException("Cannot execute a Task that is already running"); + } + + var progress = new Progress(); + + CurrentCancellationTokenSource = new CancellationTokenSource(); + + _logger.LogDebug("Executing {0}", Name); + + ((TaskManager)_taskManager).OnTaskExecuting(this); + + progress.ProgressChanged += OnProgressChanged; + + TaskCompletionStatus status; + CurrentExecutionStartTime = DateTime.UtcNow; + + Exception failureException = null; + + try + { + if (options is not null && options.MaxRuntimeTicks.HasValue) { - var trigger = triggerInfo.Item2; - trigger.Triggered -= OnTriggerTriggered; - trigger.Stop(); - if (trigger is IDisposable disposable) + CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value)); + } + + await ScheduledTask.ExecuteAsync(progress, CurrentCancellationTokenSource.Token).ConfigureAwait(false); + + status = TaskCompletionStatus.Completed; + } + catch (OperationCanceledException) + { + status = TaskCompletionStatus.Cancelled; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing Scheduled Task"); + + failureException = ex; + + status = TaskCompletionStatus.Failed; + } + + var startTime = CurrentExecutionStartTime; + var endTime = DateTime.UtcNow; + + progress.ProgressChanged -= OnProgressChanged; + CurrentCancellationTokenSource.Dispose(); + CurrentCancellationTokenSource = null; + CurrentProgress = null; + + OnTaskCompleted(startTime, endTime, status, failureException); + } + + /// + /// Progress_s the progress changed. + /// + /// The sender. + /// The e. + private void OnProgressChanged(object sender, double e) + { + e = Math.Min(e, 100); + + CurrentProgress = e; + + TaskProgress?.Invoke(this, new GenericEventArgs(e)); + } + + /// + /// Stops the task if it is currently executing. + /// + /// Cannot cancel a Task unless it is in the Running state. + public void Cancel() + { + if (State != TaskState.Running) + { + throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state."); + } + + CancelIfRunning(); + } + + /// + /// Cancels if running. + /// + public void CancelIfRunning() + { + if (State == TaskState.Running) + { + _logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name); + CurrentCancellationTokenSource.Cancel(); + } + } + + /// + /// Gets the scheduled tasks configuration directory. + /// + /// System.String. + private string GetScheduledTasksConfigurationDirectory() + { + return Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); + } + + /// + /// Gets the scheduled tasks data directory. + /// + /// System.String. + private string GetScheduledTasksDataDirectory() + { + return Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"); + } + + /// + /// Gets the history file path. + /// + /// The history file path. + private string GetHistoryFilePath() + { + return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js"); + } + + /// + /// Gets the configuration file path. + /// + /// System.String. + private string GetConfigurationFilePath() + { + return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js"); + } + + /// + /// Loads the triggers. + /// + /// IEnumerable{BaseTaskTrigger}. + private Tuple[] LoadTriggers() + { + // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly + var settings = LoadTriggerSettings().Where(i => i is not null); + + return settings.Select(i => new Tuple(i, GetTrigger(i))).ToArray(); + } + + private TaskTriggerInfo[] LoadTriggerSettings() + { + string path = GetConfigurationFilePath(); + TaskTriggerInfo[] list = null; + if (File.Exists(path)) + { + var bytes = File.ReadAllBytes(path); + list = JsonSerializer.Deserialize(bytes, _jsonOptions); + } + + // Return defaults if file doesn't exist. + return list ?? GetDefaultTriggers(); + } + + private TaskTriggerInfo[] GetDefaultTriggers() + { + try + { + return ScheduledTask.GetDefaultTriggers().ToArray(); + } + catch + { + return + [ + new() { - disposable.Dispose(); + IntervalTicks = TimeSpan.FromDays(1).Ticks, + Type = TaskTriggerInfoType.IntervalTrigger } + ]; + } + } + + /// + /// Saves the triggers. + /// + /// The triggers. + private void SaveTriggers(TaskTriggerInfo[] triggers) + { + var path = GetConfigurationFilePath(); + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream); + JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions); + } + + /// + /// Called when [task completed]. + /// + /// The start time. + /// The end time. + /// The status. + /// The exception. + private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex) + { + var elapsedTime = endTime - startTime; + + _logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds); + + var result = new TaskResult + { + StartTimeUtc = startTime, + EndTimeUtc = endTime, + Status = status, + Name = Name, + Id = Id + }; + + result.Key = ScheduledTask.Key; + + if (ex is not null) + { + result.ErrorMessage = ex.Message; + result.LongErrorMessage = ex.StackTrace; + } + + LastExecutionResult = result; + + ((TaskManager)_taskManager).OnTaskCompleted(this, result); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + DisposeTriggers(); + + var wasRunning = State == TaskState.Running; + var startTime = CurrentExecutionStartTime; + + var token = CurrentCancellationTokenSource; + if (token is not null) + { + try + { + _logger.LogInformation("{Name}: Cancelling", Name); + token.Cancel(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calling CancellationToken.Cancel();"); + } + } + + var task = _currentTask; + if (task is not null) + { + try + { + _logger.LogInformation("{Name}: Waiting on Task", Name); + var exited = task.Wait(2000); + + if (exited) + { + _logger.LogInformation("{Name}: Task exited", Name); + } + else + { + _logger.LogInformation("{Name}: Timed out waiting for task to stop", Name); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calling Task.WaitAll();"); + } + } + + if (token is not null) + { + try + { + _logger.LogDebug("{Name}: Disposing CancellationToken", Name); + token.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calling CancellationToken.Dispose();"); + } + } + + if (wasRunning) + { + OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null); + } + } + } + + /// + /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger. + /// + /// The info. + /// BaseTaskTrigger. + /// Invalid trigger type: + info.Type. + private ITaskTrigger GetTrigger(TaskTriggerInfo info) + { + var options = new TaskOptions + { + MaxRuntimeTicks = info.MaxRuntimeTicks + }; + + if (info.Type == TaskTriggerInfoType.DailyTrigger) + { + if (!info.TimeOfDayTicks.HasValue) + { + throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info)); + } + + return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options); + } + + if (info.Type == TaskTriggerInfoType.WeeklyTrigger) + { + if (!info.TimeOfDayTicks.HasValue) + { + throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info)); + } + + if (!info.DayOfWeek.HasValue) + { + throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info)); + } + + return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options); + } + + if (info.Type == TaskTriggerInfoType.IntervalTrigger) + { + if (!info.IntervalTicks.HasValue) + { + throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info)); + } + + return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options); + } + + if (info.Type == TaskTriggerInfoType.StartupTrigger) + { + return new StartupTrigger(options); + } + + throw new ArgumentException("Unrecognized trigger type: " + info.Type); + } + + /// + /// Disposes each trigger. + /// + private void DisposeTriggers() + { + foreach (var triggerInfo in InternalTriggers) + { + var trigger = triggerInfo.Item2; + trigger.Triggered -= OnTriggerTriggered; + trigger.Stop(); + if (trigger is IDisposable disposable) + { + disposable.Dispose(); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index a5e4104ffe..4ec2c9c786 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -8,255 +8,254 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks +namespace Emby.Server.Implementations.ScheduledTasks; + +/// +/// Class TaskManager. +/// +public class TaskManager : ITaskManager { /// - /// Class TaskManager. + /// The _task queue. /// - public class TaskManager : ITaskManager + private readonly ConcurrentQueue> _taskQueue = + new ConcurrentQueue>(); + + private readonly IApplicationPaths _applicationPaths; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The application paths. + /// The logger. + public TaskManager( + IApplicationPaths applicationPaths, + ILogger logger) { - /// - /// The _task queue. - /// - private readonly ConcurrentQueue> _taskQueue = - new ConcurrentQueue>(); + _applicationPaths = applicationPaths; + _logger = logger; - private readonly IApplicationPaths _applicationPaths; - private readonly ILogger _logger; + ScheduledTasks = []; + } - /// - /// Initializes a new instance of the class. - /// - /// The application paths. - /// The logger. - public TaskManager( - IApplicationPaths applicationPaths, - ILogger logger) - { - _applicationPaths = applicationPaths; - _logger = logger; + /// + public event EventHandler>? TaskExecuting; - ScheduledTasks = Array.Empty(); - } + /// + public event EventHandler? TaskCompleted; - /// - public event EventHandler>? TaskExecuting; + /// + public IReadOnlyList ScheduledTasks { get; private set; } - /// - public event EventHandler? TaskCompleted; + /// + public void CancelIfRunningAndQueue(TaskOptions options) + where T : IScheduledTask + { + var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); + ((ScheduledTaskWorker)task).CancelIfRunning(); - /// - public IReadOnlyList ScheduledTasks { get; private set; } + QueueScheduledTask(options); + } - /// - public void CancelIfRunningAndQueue(TaskOptions options) + /// + public void CancelIfRunningAndQueue() where T : IScheduledTask + { + CancelIfRunningAndQueue(new TaskOptions()); + } + + /// + public void CancelIfRunning() + where T : IScheduledTask + { + var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); + ((ScheduledTaskWorker)task).CancelIfRunning(); + } + + /// + public void QueueScheduledTask(TaskOptions options) + where T : IScheduledTask + { + var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); + + if (scheduledTask is null) { - var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); - ((ScheduledTaskWorker)task).CancelIfRunning(); - - QueueScheduledTask(options); + _logger.LogError("Unable to find scheduled task of type {Type} in QueueScheduledTask.", typeof(T).Name); } - - /// - public void CancelIfRunningAndQueue() - where T : IScheduledTask + else { - CancelIfRunningAndQueue(new TaskOptions()); + QueueScheduledTask(scheduledTask, options); } + } - /// - public void CancelIfRunning() - where T : IScheduledTask - { - var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); - ((ScheduledTaskWorker)task).CancelIfRunning(); - } + /// + public void QueueScheduledTask() + where T : IScheduledTask + { + QueueScheduledTask(new TaskOptions()); + } - /// - public void QueueScheduledTask(TaskOptions options) - where T : IScheduledTask - { - var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); + /// + public void QueueIfNotRunning() + where T : IScheduledTask + { + var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); - if (scheduledTask is null) - { - _logger.LogError("Unable to find scheduled task of type {0} in QueueScheduledTask.", typeof(T).Name); - } - else - { - QueueScheduledTask(scheduledTask, options); - } - } - - /// - public void QueueScheduledTask() - where T : IScheduledTask + if (task.State != TaskState.Running) { QueueScheduledTask(new TaskOptions()); } + } - /// - public void QueueIfNotRunning() - where T : IScheduledTask + /// + public void Execute() + where T : IScheduledTask + { + var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); + + if (scheduledTask is null) { - var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T)); - - if (task.State != TaskState.Running) - { - QueueScheduledTask(new TaskOptions()); - } + _logger.LogError("Unable to find scheduled task of type {Type} in Execute.", typeof(T).Name); } - - /// - public void Execute() - where T : IScheduledTask + else { - var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T)); + var type = scheduledTask.ScheduledTask.GetType(); - if (scheduledTask is null) - { - _logger.LogError("Unable to find scheduled task of type {0} in Execute.", typeof(T).Name); - } - else - { - var type = scheduledTask.ScheduledTask.GetType(); - - _logger.LogDebug("Queuing task {0}", type.Name); - - lock (_taskQueue) - { - if (scheduledTask.State == TaskState.Idle) - { - Execute(scheduledTask, new TaskOptions()); - } - } - } - } - - /// - public void QueueScheduledTask(IScheduledTask task, TaskOptions options) - { - var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == task.GetType()); - - if (scheduledTask is null) - { - _logger.LogError("Unable to find scheduled task of type {0} in QueueScheduledTask.", task.GetType().Name); - } - else - { - QueueScheduledTask(scheduledTask, options); - } - } - - /// - /// Queues the scheduled task. - /// - /// The task. - /// The task options. - private void QueueScheduledTask(IScheduledTaskWorker task, TaskOptions options) - { - var type = task.ScheduledTask.GetType(); - - _logger.LogDebug("Queuing task {0}", type.Name); + _logger.LogDebug("Queuing task {Name}", type.Name); lock (_taskQueue) { - if (task.State == TaskState.Idle) + if (scheduledTask.State == TaskState.Idle) { - Execute(task, options); - return; + Execute(scheduledTask, new TaskOptions()); } - - _taskQueue.Enqueue(new Tuple(type, options)); } } + } - /// - public void AddTasks(IEnumerable tasks) + /// + public void QueueScheduledTask(IScheduledTask task, TaskOptions options) + { + var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == task.GetType()); + + if (scheduledTask is null) { - var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _logger)); - - ScheduledTasks = ScheduledTasks.Concat(list).ToArray(); + _logger.LogError("Unable to find scheduled task of type {Type} in QueueScheduledTask.", task.GetType().Name); } - - /// - public void Dispose() + else { - Dispose(true); - GC.SuppressFinalize(this); + QueueScheduledTask(scheduledTask, options); } + } - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected virtual void Dispose(bool dispose) + /// + /// Queues the scheduled task. + /// + /// The task. + /// The task options. + private void QueueScheduledTask(IScheduledTaskWorker task, TaskOptions options) + { + var type = task.ScheduledTask.GetType(); + + _logger.LogDebug("Queuing task {Name}", type.Name); + + lock (_taskQueue) { - foreach (var task in ScheduledTasks) + if (task.State == TaskState.Idle) { - task.Dispose(); + Execute(task, options); + return; } + + _taskQueue.Enqueue(new Tuple(type, options)); } + } - /// - public void Cancel(IScheduledTaskWorker task) + /// + public void AddTasks(IEnumerable tasks) + { + var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _logger)); + + ScheduledTasks = ScheduledTasks.Concat(list).ToArray(); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + foreach (var task in ScheduledTasks) { - ((ScheduledTaskWorker)task).Cancel(); + task.Dispose(); } + } - /// - public Task Execute(IScheduledTaskWorker task, TaskOptions options) + /// + public void Cancel(IScheduledTaskWorker task) + { + ((ScheduledTaskWorker)task).Cancel(); + } + + /// + public Task Execute(IScheduledTaskWorker task, TaskOptions options) + { + return ((ScheduledTaskWorker)task).Execute(options); + } + + /// + /// Called when [task executing]. + /// + /// The task. + internal void OnTaskExecuting(IScheduledTaskWorker task) + { + TaskExecuting?.Invoke(this, new GenericEventArgs(task)); + } + + /// + /// Called when [task completed]. + /// + /// The task. + /// The result. + internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result) + { + TaskCompleted?.Invoke(task, new TaskCompletionEventArgs(task, result)); + + ExecuteQueuedTasks(); + } + + /// + /// Executes the queued tasks. + /// + private void ExecuteQueuedTasks() + { + lock (_taskQueue) { - return ((ScheduledTaskWorker)task).Execute(options); - } + var list = new List>(); - /// - /// Called when [task executing]. - /// - /// The task. - internal void OnTaskExecuting(IScheduledTaskWorker task) - { - TaskExecuting?.Invoke(this, new GenericEventArgs(task)); - } - - /// - /// Called when [task completed]. - /// - /// The task. - /// The result. - internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result) - { - TaskCompleted?.Invoke(task, new TaskCompletionEventArgs(task, result)); - - ExecuteQueuedTasks(); - } - - /// - /// Executes the queued tasks. - /// - private void ExecuteQueuedTasks() - { - lock (_taskQueue) + while (_taskQueue.TryDequeue(out var item)) { - var list = new List>(); - - while (_taskQueue.TryDequeue(out var item)) + if (list.All(i => i.Item1 != item.Item1)) { - if (list.All(i => i.Item1 != item.Item1)) - { - list.Add(item); - } + list.Add(item); } + } - foreach (var enqueuedType in list) + foreach (var enqueuedType in list) + { + var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == enqueuedType.Item1); + + if (scheduledTask.State == TaskState.Idle) { - var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == enqueuedType.Item1); - - if (scheduledTask.State == TaskState.Idle) - { - Execute(scheduledTask, enqueuedType.Item2); - } + Execute(scheduledTask, enqueuedType.Item2); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs index 8d1d509ff7..ef005bfaa5 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs @@ -156,14 +156,11 @@ public partial class AudioNormalizationTask : IScheduledTask /// public IEnumerable GetDefaultTriggers() { - return - [ - new TaskTriggerInfo - { - Type = TaskTriggerInfoType.IntervalTrigger, - IntervalTicks = TimeSpan.FromHours(24).Ticks - } - ]; + yield return new TaskTriggerInfo + { + Type = TaskTriggerInfoType.IntervalTrigger, + IntervalTicks = TimeSpan.FromHours(24).Ticks + }; } private async Task CalculateLUFSAsync(string inputArgs, bool waitForExit, CancellationToken cancellationToken) @@ -194,7 +191,7 @@ public partial class AudioNormalizationTask : IScheduledTask using var reader = process.StandardError; float? lufs = null; - await foreach (var line in reader.ReadAllLinesAsync(cancellationToken)) + await foreach (var line in reader.ReadAllLinesAsync(cancellationToken).ConfigureAwait(false)) { Match match = LUFSRegex().Match(line); if (match.Success) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index b76fdeeb04..f81309560e 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -17,155 +17,151 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.ScheduledTasks.Tasks +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// +/// Class ChapterImagesTask. +/// +public class ChapterImagesTask : IScheduledTask { + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly IApplicationPaths _appPaths; + private readonly IChapterManager _chapterManager; + private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; + /// - /// Class ChapterImagesTask. + /// Initializes a new instance of the class. /// - public class ChapterImagesTask : IScheduledTask + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ChapterImagesTask( + ILogger logger, + ILibraryManager libraryManager, + IApplicationPaths appPaths, + IChapterManager chapterManager, + IFileSystem fileSystem, + ILocalizationManager localization) { - private readonly ILogger _logger; - private readonly ILibraryManager _libraryManager; - private readonly IApplicationPaths _appPaths; - private readonly IChapterManager _chapterManager; - private readonly IFileSystem _fileSystem; - private readonly ILocalizationManager _localization; + _logger = logger; + _libraryManager = libraryManager; + _appPaths = appPaths; + _chapterManager = chapterManager; + _fileSystem = fileSystem; + _localization = localization; + } - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public ChapterImagesTask( - ILogger logger, - ILibraryManager libraryManager, - IApplicationPaths appPaths, - IChapterManager chapterManager, - IFileSystem fileSystem, - ILocalizationManager localization) + /// + public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages"); + + /// + public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription"); + + /// + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + + /// + public string Key => "RefreshChapterImages"; + + /// + public IEnumerable GetDefaultTriggers() + { + yield return new TaskTriggerInfo { - _logger = logger; - _libraryManager = libraryManager; - _appPaths = appPaths; - _chapterManager = chapterManager; - _fileSystem = fileSystem; - _localization = localization; - } + Type = TaskTriggerInfoType.DailyTrigger, + TimeOfDayTicks = TimeSpan.FromHours(2).Ticks, + MaxRuntimeTicks = TimeSpan.FromHours(4).Ticks + }; + } - /// - public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages"); - - /// - public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription"); - - /// - public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); - - /// - public string Key => "RefreshChapterImages"; - - /// - public IEnumerable GetDefaultTriggers() + /// + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + var videos = _libraryManager.GetItemList(new InternalItemsQuery { - return - [ - new TaskTriggerInfo - { - Type = TaskTriggerInfoType.DailyTrigger, - TimeOfDayTicks = TimeSpan.FromHours(2).Ticks, - MaxRuntimeTicks = TimeSpan.FromHours(4).Ticks - } - ]; - } - - /// - public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) - { - var videos = _libraryManager.GetItemList(new InternalItemsQuery + MediaTypes = [MediaType.Video], + IsFolder = false, + Recursive = true, + DtoOptions = new DtoOptions(false) { - MediaTypes = [MediaType.Video], - IsFolder = false, - Recursive = true, - DtoOptions = new DtoOptions(false) - { - EnableImages = false - }, - SourceTypes = [SourceType.Library], - IsVirtualItem = false - }) - .OfType