Compare commits

...

17 Commits

Author SHA1 Message Date
TheMelmacian 6ac7d5e304
Merge edd5faf94c into 5612cb8178 2024-04-25 09:15:45 -04:00
renovate[bot] 5612cb8178
chore(deps): update ci dependencies (#11427)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-25 07:15:31 -06:00
renovate[bot] ccd06bc547
chore(deps): update dependency diacritics to v3.3.29 (#11429)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-25 07:02:45 -06:00
Bond-009 d29b85a134
Fix multiple intro providers and remove unneeded ToLists (#11431) 2024-04-25 07:02:01 -06:00
TheMelmacian edd5faf94c Integrate branch 'master' into feature/language_filters 2024-03-23 15:23:34 +01:00
TheMelmacian 5eb068c603 Integrate branch 'master' into feature/language_filters 2024-03-04 01:14:10 +01:00
TheMelmacian 677aa508ea Integrate branch 'master' into feature/language_filters 2024-02-10 12:25:19 +01:00
TheMelmacian 27dcb78b04 Integrate branch 'master' into feature/language_filters 2024-01-21 23:24:24 +01:00
TheMelmacian 068ae48313 Integrate branch 'master' into feature/language_filters 2023-11-27 23:02:53 +01:00
TheMelmacian fb58c28676 fix potential sql injection 2023-11-26 23:08:47 +01:00
TheMelmacian 1f4acf10d7 apply suggested changes from code review 2023-11-25 20:49:15 +01:00
TheMelmacian 828c05e379 move database changes to MigrationRoutine 2023-11-25 20:49:15 +01:00
TheMelmacian db6e68a8e6 use SQL instead of post-filtering for HasSubtitles 2023-11-25 20:49:15 +01:00
TheMelmacian 12bfeb3e73 alter column type of OwnerId and add indexes 2023-11-25 20:49:15 +01:00
TheMelmacian 014a1afd86 add filters for audio and subtitle languages 2023-11-25 20:49:15 +01:00
TheMelmacian 6aa53748a7 extend filter logic to include alternate versions 2023-11-25 20:49:15 +01:00
TheMelmacian 2ec438ddde provide language tags as filter values 2023-11-25 20:49:15 +01:00
22 changed files with 327 additions and 75 deletions

View File

@ -20,7 +20,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Setup .NET
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:

View File

@ -14,7 +14,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -39,7 +39,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -78,12 +78,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@9c19ed7fe5d278cd354c7dfd5d3b88589c7e2395 # v4.1.6
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
uses: actions/download-artifact@9c19ed7fe5d278cd354c7dfd5d3b88589c7e2395 # v4.1.6
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: openapi-base
path: openapi-base
@ -152,7 +152,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@9c19ed7fe5d278cd354c7dfd5d3b88589c7e2395 # v4.1.6
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: openapi-head
path: openapi-head

View File

@ -19,7 +19,7 @@ jobs:
runs-on: "${{ matrix.os }}"
steps:
- uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:

View File

@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@ -51,7 +51,7 @@ jobs:
reactions: eyes
- name: Checkout the latest code
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@ -128,7 +128,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: pull in script
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
repository: jellyfin/jellyfin-triage-script
- name: install python

View File

@ -10,7 +10,7 @@ jobs:
issues: write
steps:
- name: pull in script
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
repository: jellyfin/jellyfin-triage-script
- name: install python

View File

@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8
- name: Checkout Repository
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
ref: ${{ env.TAG_BRANCH }}
@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # v4.1.3
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
ref: ${{ env.TAG_BRANCH }}

View File

@ -177,6 +177,8 @@
- [Chris-Codes-It](https://github.com/Chris-Codes-It)
- [Pithaya](https://github.com/Pithaya)
- [Çağrı Sakaoğlu](https://github.com/ilovepilav)
- [TheMelmacian](https://github.com/TheMelmacian)
_ [Barasingha](https://github.com/MaVdbussche)
- [Barasingha](https://github.com/MaVdbussche)
- [Gauvino](https://github.com/Gauvino)
- [felix920506](https://github.com/felix920506)

View File

@ -13,7 +13,7 @@
<PackageVersion Include="BlurHashSharp" Version="1.3.2" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Diacritics" Version="3.3.27" />
<PackageVersion Include="Diacritics" Version="3.3.29" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.4.3" />

View File

@ -408,6 +408,10 @@ namespace Emby.Server.Implementations.Data
// resume
"create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)",
// used to effectively link items when building item tree
"create index if not exists idx_TypedBaseItemsOwnerId on TypedBaseItems(OwnerId)",
"create index if not exists idx_TypedBaseItemsPrimaryVersionId on TypedBaseItems(PrimaryVersionId)",
// items by name
"create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)",
"create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)",
@ -497,7 +501,7 @@ namespace Emby.Server.Implementations.Data
AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "OwnerId", "GUID", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames);
@ -3868,41 +3872,9 @@ namespace Emby.Server.Implementations.Data
}
}
if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage))
{
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Audio' and MediaStreams.Language=@HasNoAudioTrackWithLanguage limit 1) is null)");
statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage);
}
if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage))
{
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=0 and MediaStreams.Language=@HasNoInternalSubtitleTrackWithLanguage limit 1) is null)");
statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage);
}
if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage))
{
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.IsExternal=1 and MediaStreams.Language=@HasNoExternalSubtitleTrackWithLanguage limit 1) is null)");
statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage);
}
if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage))
{
whereClauses.Add("((select language from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' and MediaStreams.Language=@HasNoSubtitleTrackWithLanguage limit 1) is null)");
statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage);
}
if (query.HasSubtitles.HasValue)
{
if (query.HasSubtitles.Value)
{
whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) not null)");
}
else
{
whereClauses.Add("((select type from MediaStreams where MediaStreams.ItemId=A.Guid and MediaStreams.StreamType='Subtitle' limit 1) is null)");
}
}
// if no filters are present for MediaStreams the method returns (1=1)
// which can be added to the where clause without changing the result and no null or empty check is needed
whereClauses.Add(GetMediaStreamsFiltersSubquery(query, statement));
if (query.HasChapterImages.HasValue)
{
@ -4316,6 +4288,115 @@ namespace Emby.Server.Implementations.Data
return whereClauses;
}
private string GetMediaStreamsFiltersSubquery(InternalItemsQuery query, SqliteCommand? statement)
{
var mediaStreamsQuery = @"
WITH RECURSIVE items AS (
SELECT
tbi.guid AS CollectionId,
tbi.""type"" AS CollectionType,
0 AS ""Level"",
tbi.Guid AS ItemId,
tbi.""type"" AS ItemType,
tbi.PresentationUniqueKey as ItemPresentationUniqueKey,
tbi.PrimaryVersionId as ItemPrimaryVersionId,
null AS CollectionParent
FROM TypedBaseItems tbi
WHERE tbi.guid = A.Guid
UNION ALL
SELECT
CollectionId,
CollectionType,
""Level"" + 1 AS ""Level"",
child.Guid AS ItemId,
child.""type"" AS ItemType,
child.PresentationUniqueKey AS ItemPresentationUniqueKey,
child.PrimaryVersionId as ItemPrimaryVersionId,
i.ItemId AS CollectionParent
FROM TypedBaseItems child
JOIN items i
ON child.ParentId = i.ItemId
OR (child.PrimaryVersionId = i.ItemPresentationUniqueKey AND i.ItemPrimaryVersionId IS NULL)
OR (child.OwnerId = i.ItemId AND child.ExtraType IS NULL)
)
";
var mediaStreamsFilters = new List<string>();
if (!string.IsNullOrWhiteSpace(query.HasNoAudioTrackWithLanguage))
{
mediaStreamsFilters.Add("SELECT CASE WHEN NOT EXISTS (select 1 from items join mediastreams ms ON ms.ItemId = items.ItemId AND ms.StreamType = 'Audio' AND ms.Language = @HasNoAudioTrackWithLanguage LIMIT 1) THEN TRUE ELSE FALSE END AS StreamFilterMatches");
statement?.TryBind("@HasNoAudioTrackWithLanguage", query.HasNoAudioTrackWithLanguage);
}
if (!string.IsNullOrWhiteSpace(query.HasNoInternalSubtitleTrackWithLanguage))
{
mediaStreamsFilters.Add("SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM items join mediastreams ms ON ms.ItemId = items.ItemId AND ms.StreamType = 'Subtitle' AND ms.IsExternal = 0 AND ms.Language = @HasNoInternalSubtitleTrackWithLanguage LIMIT 1) THEN TRUE ELSE FALSE END AS StreamFilterMatches");
statement?.TryBind("@HasNoInternalSubtitleTrackWithLanguage", query.HasNoInternalSubtitleTrackWithLanguage);
}
if (!string.IsNullOrWhiteSpace(query.HasNoExternalSubtitleTrackWithLanguage))
{
mediaStreamsFilters.Add("SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM items join mediastreams ms ON ms.ItemId = items.ItemId AND ms.StreamType = 'Subtitle' AND ms.IsExternal = 1 AND ms.Language = @HasNoExternalSubtitleTrackWithLanguage LIMIT 1) THEN TRUE ELSE FALSE END AS StreamFilterMatches");
statement?.TryBind("@HasNoExternalSubtitleTrackWithLanguage", query.HasNoExternalSubtitleTrackWithLanguage);
}
if (!string.IsNullOrWhiteSpace(query.HasNoSubtitleTrackWithLanguage))
{
mediaStreamsFilters.Add("SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM items join mediastreams ms ON ms.ItemId = items.ItemId AND ms.StreamType = 'Subtitle' AND ms.Language = @HasNoSubtitleTrackWithLanguage LIMIT 1) THEN TRUE ELSE FALSE END AS StreamFilterMatches");
statement?.TryBind("@HasNoSubtitleTrackWithLanguage", query.HasNoSubtitleTrackWithLanguage);
}
if (query.HasSubtitles.HasValue)
{
if (query.HasSubtitles.Value)
{
mediaStreamsFilters.Add("SELECT CASE WHEN EXISTS (SELECT 1 FROM items JOIN mediastreams ms ON ms.ItemId = items.ItemId AND ms.StreamType = 'Subtitle' limit 1) THEN TRUE ELSE FALSE END AS StreamFilterMatches");
}
else
{
mediaStreamsFilters.Add("SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM items JOIN mediastreams ms ON ms.ItemId = items.ItemId AND ms.StreamType = 'Subtitle' limit 1) THEN TRUE ELSE FALSE END AS StreamFilterMatches");
}
}
if (query.AudioLanguage.Length > 0)
{
var languages = string.Join(", ", query.AudioLanguage.Select((lang, index) => $"@AudioLanguage_{index}"));
var undefinedLanguage = query.AudioLanguage.Contains("und") ? "or ms.Language is null" : string.Empty; // language with null value is handled as unddefined
mediaStreamsFilters.Add("SELECT CASE WHEN EXISTS (SELECT 1 FROM items JOIN MediaStreams ms ON ms.ItemId = items.ItemId AND ms.StreamType = 'Audio' AND (ms.Language in (" + languages + ") " + undefinedLanguage + ") limit 1) THEN TRUE ELSE FALSE END AS StreamFilterMatches");
var i = 0;
foreach (var lang in query.AudioLanguage)
{
statement?.TryBind($"@AudioLanguage_{i++}", lang);
}
}
if (query.SubtitleLanguage.Length > 0)
{
var languages = string.Join(", ", query.SubtitleLanguage.Select((lang, index) => $"@SubtitleLanguage_{index}"));
var undefinedLanguage = query.SubtitleLanguage.Contains("und") ? "or ms.Language is null" : string.Empty; // language with null value is handled as unddefined
mediaStreamsFilters.Add("SELECT CASE WHEN EXISTS (SELECT 1 FROM items JOIN MediaStreams ms ON ms.ItemId = items.ItemId AND ms.StreamType = 'Subtitle' AND (ms.Language in (" + languages + ") " + undefinedLanguage + ") limit 1) THEN TRUE ELSE FALSE END AS StreamFilterMatches");
var i = 0;
foreach (var lang in query.SubtitleLanguage)
{
statement?.TryBind($"@SubtitleLanguage_{i++}", lang);
}
}
if (mediaStreamsFilters.Count > 0)
{
return "(NOT EXISTS (" + mediaStreamsQuery + " SELECT 1 FROM (" + string.Join(" UNION ", mediaStreamsFilters) + ") WHERE StreamFilterMatches = FALSE LIMIT 1))";
}
else
{
return "(1=1)"; // -> resolves always to true; can be added to the where clause without null or empty check
}
}
/// <summary>
/// Formats a where clause for the specified provider.
/// </summary>
@ -4903,7 +4984,9 @@ AND Type = @InternalPersonType)");
NameContains = query.NameContains,
SearchTerm = query.SearchTerm,
SimilarTo = query.SimilarTo,
ExcludeItemIds = query.ExcludeItemIds
ExcludeItemIds = query.ExcludeItemIds,
AudioLanguage = query.AudioLanguage,
SubtitleLanguage = query.SubtitleLanguage
};
var outerWhereClauses = GetWhereClauses(outerQuery, null);

View File

@ -44,7 +44,6 @@ using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
using TMDbLib.Objects.Authentication;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
using Genre = MediaBrowser.Controller.Entities.Genre;
@ -1612,14 +1611,18 @@ namespace Emby.Server.Implementations.Library
/// <returns>IEnumerable{System.String}.</returns>
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
{
if (IntroProviders.Length == 0)
{
return [];
}
var tasks = IntroProviders
.Take(1)
.Select(i => GetIntros(i, item, user));
var items = await Task.WhenAll(tasks).ConfigureAwait(false);
return items
.SelectMany(i => i.ToArray())
.SelectMany(i => i)
.Select(ResolveIntro)
.Where(i => i is not null)!; // null values got filtered out
}

View File

@ -274,7 +274,7 @@ namespace Emby.Server.Implementations.Library
var tasks = _providers.Select(i => GetDynamicMediaSources(item, i, cancellationToken));
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
return results.SelectMany(i => i.ToList());
return results.SelectMany(i => i);
}
private async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, IMediaSourceProvider provider, CancellationToken cancellationToken)

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@ -8,6 +9,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -89,6 +91,8 @@ public class FilterController : BaseJellyfinApiController
}
var itemList = folder.GetItemList(query);
var mediaStreams = GetMediaStreams(itemList, BaseItemKind.Movie, BaseItemKind.Episode).ToList();
return new QueryFiltersLegacy
{
Years = itemList.Select(i => i.ProductionYear ?? -1)
@ -113,6 +117,20 @@ public class FilterController : BaseJellyfinApiController
.Where(i => !string.IsNullOrWhiteSpace(i))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Order()
.ToArray(),
AudioLanguages = mediaStreams
.Where(mediaStream => mediaStream.Type == MediaStreamType.Audio)
.Select(mediaStream => string.IsNullOrWhiteSpace(mediaStream.Language) ? "und" : mediaStream.Language)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Order()
.ToArray(),
SubtitleLanguages = mediaStreams
.Where(mediaStream => mediaStream.Type == MediaStreamType.Subtitle)
.Select(mediaStream => string.IsNullOrWhiteSpace(mediaStream.Language) ? "und" : mediaStream.Language)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Order()
.ToArray()
};
}
@ -215,4 +233,32 @@ public class FilterController : BaseJellyfinApiController
return filters;
}
/// <summary>
/// Extracts the MediaStreams from the list of BaseItems.
/// If an item is a Folder, MediaStreams are searched in the list of it's child items.
/// </summary>
/// <param name="itemList">The list of items for which the MediaStreams should be xtracted.</param>
/// <param name="types">Optional list of item kinds for which the MediaStreams should be extracted.</param>
/// <returns>A list of all MediaStreams which are linked to the provided items.</returns>
private IEnumerable<MediaStream> GetMediaStreams(IEnumerable<BaseItem> itemList, params BaseItemKind[] types)
{
return itemList
.Where(item => item.IsFolder || types.Length == 0 || types.Contains(item.GetBaseItemKind()))
.SelectMany(item =>
{
if (item.IsFolder)
{
return GetMediaStreams(((Folder)item).Children, types);
}
else if (item is Video)
{
return ((Video)item).GetAllLinkedMediaStreams();
}
else
{
return item.GetMediaStreams();
}
});
}
}

View File

@ -151,6 +151,8 @@ public class ItemsController : BaseJellyfinApiController
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="audioLanguages">Optional. If specified, results will be filtered based on audio language. This allows multiple, comma delimited values.</param>
/// <param name="subtitleLanguages">Optional. If specified, results will be filtered based on subtitale language. This allows multiple, comma delimited values.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
@ -240,6 +242,8 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] audioLanguages,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] subtitleLanguages,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
@ -368,6 +372,8 @@ public class ItemsController : BaseJellyfinApiController
MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
MinPremiereDate = minPremiereDate?.ToUniversalTime(),
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
AudioLanguage = audioLanguages,
SubtitleLanguage = subtitleLanguages,
};
if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
@ -608,6 +614,8 @@ public class ItemsController : BaseJellyfinApiController
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="audioLanguages">Optional. If specified, results will be filtered based on audio language. This allows multiple, comma delimited values.</param>
/// <param name="subtitleLanguages">Optional. If specified, results will be filtered based on subtitale language. This allows multiple, comma delimited values.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
@ -699,6 +707,8 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] audioLanguages,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] subtitleLanguages,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
=> GetItems(
@ -785,6 +795,8 @@ public class ItemsController : BaseJellyfinApiController
nameLessThan,
studioIds,
genreIds,
audioLanguages,
subtitleLanguages,
enableTotalRecordCount,
enableImages);

View File

@ -112,6 +112,8 @@ public class TrailersController : BaseJellyfinApiController
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="audioLanguages">Optional. If specified, results will be filtered based on audio language. This allows multiple, comma delimited values.</param>
/// <param name="subtitleLanguages">Optional. If specified, results will be filtered based on subtitale language. This allows multiple, comma delimited values.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
@ -200,6 +202,8 @@ public class TrailersController : BaseJellyfinApiController
[FromQuery] string? nameLessThan,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] audioLanguages,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] subtitleLanguages,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true)
{
@ -290,6 +294,8 @@ public class TrailersController : BaseJellyfinApiController
nameLessThan,
studioIds,
genreIds,
audioLanguages,
subtitleLanguages,
enableTotalRecordCount,
enableImages);
}

View File

@ -44,7 +44,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.FixPlaylistOwner),
typeof(Routines.MigrateRatingLevels),
typeof(Routines.AddDefaultCastReceivers),
typeof(Routines.UpdateDefaultPluginRepository)
typeof(Routines.UpdateDefaultPluginRepository),
typeof(Routines.ChangeTypeOfTypedBaseItemsOwnerIdToGuid)
};
/// <summary>

View File

@ -0,0 +1,96 @@
using System;
using System.Globalization;
using System.IO;
using Emby.Server.Implementations.Data;
using MediaBrowser.Controller;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines
{
/// <summary>
/// Change type of TypedBaseItems.OwnerId to GUID so database can use index when compared to guid column.
/// </summary>
internal class ChangeTypeOfTypedBaseItemsOwnerIdToGuid : IMigrationRoutine
{
private const string DbFilename = "library.db";
private readonly ILogger<ChangeTypeOfTypedBaseItemsOwnerIdToGuid> _logger;
private readonly IServerApplicationPaths _paths;
public ChangeTypeOfTypedBaseItemsOwnerIdToGuid(ILogger<ChangeTypeOfTypedBaseItemsOwnerIdToGuid> logger, IServerApplicationPaths paths)
{
_logger = logger;
_paths = paths;
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("{9ACD7444-568D-4E20-89A1-B0E0D94023AC}");
/// <inheritdoc/>
public string Name => "ChangeTypeOfTypedBaseItemsOwnerIdToGuid";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/>
public void Perform()
{
var dataPath = _paths.DataPath;
var dbPath = Path.Combine(dataPath, DbFilename);
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
{
// Back up the database before column is changed
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;
}
}
}
// Determine the type of TypedBaseItems.OwnerId
_logger.LogInformation("Determine type of column TypedBaseItems.OwnerId.");
var result = connection.Query("SELECT type FROM pragma_table_info('TypedBaseItems') WHERE name = 'OwnerId'").GetEnumerator();
if (result.MoveNext())
{
var row = result.Current;
// If the type is TEXT change it to GUID so the database can use indexes when OwnerId is compared to guid column
if (row.TryGetString(0, out var columnType) && columnType != "GUID")
{
_logger.LogInformation("Type of column TypedBaseItems.OwnerId is {ColumnType} -> changing to GUID.", columnType);
// use separate connections for alter table commands to prevent sqlite "database table is locked" exception
using (var alterTableConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
using (var transaction = alterTableConnection.BeginTransaction())
{
alterTableConnection.Execute("DROP INDEX IF EXISTS idx_TypedBaseItemsOwnerId");
alterTableConnection.Execute("ALTER TABLE TypedBaseItems RENAME COLUMN OwnerId TO OwnerId_OLD");
alterTableConnection.Execute("ALTER TABLE TypedBaseItems ADD COLUMN OwnerId GUID NULL");
alterTableConnection.Execute("UPDATE TypedBaseItems SET OwnerId = OwnerId_OLD WHERE OwnerId_OLD IS NOT NULL");
alterTableConnection.Execute("ALTER TABLE TypedBaseItems DROP COLUMN OwnerId_OLD");
alterTableConnection.Execute("CREATE INDEX idx_TypedBaseItemsOwnerId ON TypedBaseItems(OwnerId)");
transaction.Commit();
}
}
else
{
// Do nothing if the column is already of type GUID
_logger.LogInformation("Type of column TypedBaseItems.OwnerId is GUID -> no migration necessary");
}
}
}
}
}
}

View File

@ -825,12 +825,6 @@ namespace MediaBrowser.Controller.Entities
return true;
}
if (query.HasSubtitles.HasValue)
{
Logger.LogDebug("Query requires post-filtering due to HasSubtitles");
return true;
}
if (query.HasTrailer.HasValue)
{
Logger.LogDebug("Query requires post-filtering due to HasTrailer");

View File

@ -51,6 +51,8 @@ namespace MediaBrowser.Controller.Entities
TrailerTypes = Array.Empty<TrailerType>();
VideoTypes = Array.Empty<VideoType>();
Years = Array.Empty<int>();
AudioLanguage = Array.Empty<string>();
SubtitleLanguage = Array.Empty<string>();
}
public InternalItemsQuery(User? user)
@ -358,6 +360,10 @@ namespace MediaBrowser.Controller.Entities
public string? SeriesTimerId { get; set; }
public string[] AudioLanguage { get; set; }
public string[] SubtitleLanguage { get; set; }
public void SetUser(User user)
{
MaxParentalRating = user.MaxParentalAgeRating;

View File

@ -683,18 +683,6 @@ namespace MediaBrowser.Controller.Entities
}
}
if (query.HasSubtitles.HasValue)
{
var val = query.HasSubtitles.Value;
var video = item as Video;
if (video is null || val != video.HasSubtitles)
{
return false;
}
}
if (query.HasParentalRating.HasValue)
{
var val = query.HasParentalRating.Value;

View File

@ -379,6 +379,15 @@ namespace MediaBrowser.Controller.Entities
.OrderBy(i => i.SortName);
}
/// <summary>
/// Gets the MediaStreams for this Video and all alternate Versions (linked and local).
/// </summary>
/// <returns>A list of all MediaStreams which are linked to this Video.</returns>
public IEnumerable<MediaStream> GetAllLinkedMediaStreams()
{
return GetAllItemsForMediaSources().SelectMany(item => item.Item.GetMediaStreams());
}
/// <summary>
/// Gets the additional parts.
/// </summary>

View File

@ -13,6 +13,8 @@ namespace MediaBrowser.Model.Querying
Tags = Array.Empty<string>();
OfficialRatings = Array.Empty<string>();
Years = Array.Empty<int>();
AudioLanguages = Array.Empty<string>();
SubtitleLanguages = Array.Empty<string>();
}
public string[] Genres { get; set; }
@ -22,5 +24,9 @@ namespace MediaBrowser.Model.Querying
public string[] OfficialRatings { get; set; }
public int[] Years { get; set; }
public string[] AudioLanguages { get; set; }
public string[] SubtitleLanguages { get; set; }
}
}

View File

@ -286,7 +286,7 @@ namespace MediaBrowser.Providers.Manager
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
return results.SelectMany(i => i.ToList());
return results.SelectMany(i => i);
}
/// <summary>