mirror of https://github.com/jellyfin/jellyfin.git
Compare commits
17 Commits
9d6cafa8f0
...
6ac7d5e304
Author | SHA1 | Date |
---|---|---|
TheMelmacian | 6ac7d5e304 | |
renovate[bot] | 5612cb8178 | |
renovate[bot] | ccd06bc547 | |
Bond-009 | d29b85a134 | |
TheMelmacian | edd5faf94c | |
TheMelmacian | 5eb068c603 | |
TheMelmacian | 677aa508ea | |
TheMelmacian | 27dcb78b04 | |
TheMelmacian | 068ae48313 | |
TheMelmacian | fb58c28676 | |
TheMelmacian | 1f4acf10d7 | |
TheMelmacian | 828c05e379 | |
TheMelmacian | db6e68a8e6 | |
TheMelmacian | 12bfeb3e73 | |
TheMelmacian | 014a1afd86 | |
TheMelmacian | 6aa53748a7 | |
TheMelmacian | 2ec438ddde |
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue