mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-05-18 14:18:11 +02:00
Compare commits
18 commits
12a99c7e14
...
39b27fd8eb
Author | SHA1 | Date | |
---|---|---|---|
39b27fd8eb | |||
e2a22cec0e | |||
067962ae2a | |||
8a65d239b7 | |||
518404cd1d | |||
edd5faf94c | |||
5eb068c603 | |||
677aa508ea | |||
27dcb78b04 | |||
068ae48313 | |||
fb58c28676 | |||
1f4acf10d7 | |||
828c05e379 | |||
db6e68a8e6 | |||
12bfeb3e73 | |||
014a1afd86 | |||
6aa53748a7 | |||
2ec438ddde |
|
@ -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)
|
||||
|
|
|
@ -409,6 +409,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)",
|
||||
|
@ -499,7 +503,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);
|
||||
|
@ -3866,41 +3870,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)
|
||||
{
|
||||
|
@ -4326,6 +4298,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>
|
||||
|
@ -4913,7 +4994,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);
|
||||
|
|
|
@ -128,5 +128,7 @@
|
|||
"TaskRefreshTrickplayImages": "توليد صور Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.",
|
||||
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة."
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
|
||||
"TaskAudioNormalization": "تطبيع الصوت",
|
||||
"TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت."
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"Albums": "Albumok",
|
||||
"AppDeviceValues": "Program: {0}, eszköz: {1}",
|
||||
"AppDeviceValues": "Program: {0}, Eszköz: {1}",
|
||||
"Application": "Alkalmazás",
|
||||
"Artists": "Előadók",
|
||||
"AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
|
||||
"Books": "Könyvek",
|
||||
"CameraImageUploadedFrom": "Új kamerakép feltöltve innen: {0}",
|
||||
"CameraImageUploadedFrom": "Új kamerakép lett feltöltve innen: {0}",
|
||||
"Channels": "Csatornák",
|
||||
"ChapterNameValue": "{0}. jelenet",
|
||||
"ChapterNameValue": "Jelenet {0}",
|
||||
"Collections": "Gyűjtemények",
|
||||
"DeviceOfflineWithName": "{0} kijelentkezett",
|
||||
"DeviceOnlineWithName": "{0} belépett",
|
||||
|
@ -15,27 +15,27 @@
|
|||
"Favorites": "Kedvencek",
|
||||
"Folders": "Könyvtárak",
|
||||
"Genres": "Műfajok",
|
||||
"HeaderAlbumArtists": "Albumelőadók",
|
||||
"HeaderAlbumArtists": "Album előadók",
|
||||
"HeaderContinueWatching": "Megtekintés folytatása",
|
||||
"HeaderFavoriteAlbums": "Kedvenc albumok",
|
||||
"HeaderFavoriteAlbums": "Kedvenc Albumok",
|
||||
"HeaderFavoriteArtists": "Kedvenc előadók",
|
||||
"HeaderFavoriteEpisodes": "Kedvenc epizódok",
|
||||
"HeaderFavoriteShows": "Kedvenc sorozatok",
|
||||
"HeaderFavoriteSongs": "Kedvenc számok",
|
||||
"HeaderFavoriteSongs": "Kedvenc dalok",
|
||||
"HeaderLiveTV": "Élő TV",
|
||||
"HeaderNextUp": "Következik",
|
||||
"HeaderRecordingGroups": "Felvételi csoportok",
|
||||
"HomeVideos": "Házi videók",
|
||||
"HomeVideos": "Otthoni videók",
|
||||
"Inherit": "Örökölt",
|
||||
"ItemAddedWithName": "{0} hozzáadva a könyvtárhoz",
|
||||
"ItemRemovedWithName": "{0} eltávolítva a könyvtárból",
|
||||
"LabelIpAddressValue": "IP-cím: {0}",
|
||||
"LabelRunningTimeValue": "Lejátszási idő: {0}",
|
||||
"Latest": "Legújabb",
|
||||
"MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve",
|
||||
"MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve lett",
|
||||
"MessageApplicationUpdatedTo": "A Jellyfin kiszolgáló frissítve lett a következőre: {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve: {0}",
|
||||
"MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve lett: {0}",
|
||||
"MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve lett",
|
||||
"MixedContent": "Vegyes tartalom",
|
||||
"Movies": "Filmek",
|
||||
"Music": "Zenék",
|
||||
|
@ -46,7 +46,7 @@
|
|||
"NewVersionIsAvailable": "Letölthető a Jellyfin kiszolgáló új verziója.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Frissítés érhető el az alkalmazáshoz",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Alkalmazásfrissítés telepítve",
|
||||
"NotificationOptionAudioPlayback": "Hanglejátszás elkezdve",
|
||||
"NotificationOptionAudioPlayback": "Hanglejátszás elkezdődött",
|
||||
"NotificationOptionAudioPlaybackStopped": "Hanglejátszás leállítva",
|
||||
"NotificationOptionCameraImageUploaded": "Kamerakép feltöltve",
|
||||
"NotificationOptionInstallationFailed": "Telepítési hiba",
|
||||
|
@ -126,5 +126,9 @@
|
|||
"External": "Külső",
|
||||
"HearingImpaired": "Hallássérült",
|
||||
"TaskRefreshTrickplayImages": "Trickplay képek generálása",
|
||||
"TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz."
|
||||
"TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz.",
|
||||
"TaskAudioNormalization": "Hangerő Normalizáció",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Nem létező elemek törlése a gyűjteményekből és lejátszási listákról.",
|
||||
"TaskAudioNormalizationDescription": "Hangerő normalizációs adatok keresése.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Gyűjtemények és lejátszási listák optimalizálása"
|
||||
}
|
||||
|
|
|
@ -128,5 +128,7 @@
|
|||
"TaskRefreshTrickplayImages": "Trickplay Görselleri Oluştur",
|
||||
"TaskRefreshTrickplayImagesDescription": "Etkin kütüphanelerdeki videolar için trickplay önizlemeleri oluşturur.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Artık var olmayan koleksiyon ve çalma listelerindeki ögeleri kaldırır.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Koleksiyonları ve çalma listelerini temizleyin"
|
||||
"TaskCleanCollectionsAndPlaylists": "Koleksiyonları ve çalma listelerini temizleyin",
|
||||
"TaskAudioNormalizationDescription": "Ses normalleştirme verileri için dosyaları tarar.",
|
||||
"TaskAudioNormalization": "Ses Normalleştirme"
|
||||
}
|
||||
|
|
|
@ -128,5 +128,7 @@
|
|||
"TaskRefreshTrickplayImages": "生成时间轴缩略图",
|
||||
"TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。",
|
||||
"TaskCleanCollectionsAndPlaylists": "清理合集和播放列表",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。"
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。",
|
||||
"TaskAudioNormalization": "音频标准化",
|
||||
"TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
@ -375,6 +379,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))
|
||||
|
@ -615,6 +621,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>
|
||||
|
@ -706,6 +714,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(
|
||||
|
@ -792,6 +802,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; }
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue