Compare commits

...

18 commits

Author SHA1 Message Date
TheMelmacian 39b27fd8eb
Merge edd5faf94c into e2a22cec0e 2024-05-04 13:06:58 -04:00
Nyanmisaka e2a22cec0e Translated using Weblate (Chinese (Simplified))
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/
2024-05-04 13:06:46 -04:00
queeup 067962ae2a Translated using Weblate (Turkish)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/tr/
2024-05-04 13:06:46 -04:00
Szilágyi Kristóf 8a65d239b7 Translated using Weblate (Hungarian)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/
2024-05-04 13:06:46 -04:00
HiPotionQ8 518404cd1d Translated using Weblate (Arabic)
Translation: Jellyfin/Jellyfin
Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/
2024-05-04 13:06:46 -04: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
16 changed files with 330 additions and 71 deletions

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

@ -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);

View file

@ -128,5 +128,7 @@
"TaskRefreshTrickplayImages": "توليد صور Trickplay",
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.",
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة."
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
"TaskAudioNormalization": "تطبيع الصوت",
"TaskAudioNormalizationDescription": "مسح الملفات لتطبيع بيانات الصوت."
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -128,5 +128,7 @@
"TaskRefreshTrickplayImages": "生成时间轴缩略图",
"TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。",
"TaskCleanCollectionsAndPlaylists": "清理合集和播放列表",
"TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。"
"TaskCleanCollectionsAndPlaylistsDescription": "清理合集和播放列表中已不存在的项目。",
"TaskAudioNormalization": "音频标准化",
"TaskAudioNormalizationDescription": "扫描文件以寻找音频标准化数据。"
}

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)
{
@ -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);

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; }
}
}