Merge remote-tracking branch 'upstream/master' into fix-byname-queries

# Conflicts:
#	Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
This commit is contained in:
Shadowghost
2026-08-05 12:15:30 +02:00
21 changed files with 251 additions and 28 deletions
@@ -100,7 +100,7 @@
"TaskAudioNormalization": "تطبيع الصوت",
"TaskAudioNormalizationDescription": "يفحص الملفات لجمع بيانات تطبيع الصوت.",
"TaskDownloadMissingLyrics": "تنزيل الكلمات المفقودة",
"TaskDownloadMissingLyricsDescription": "ينزّل الكلمات للأغاني.",
"TaskDownloadMissingLyricsDescription": "تحميل كلمات الأغاني",
"TaskExtractMediaSegments": "فحص مقاطع المحتوى",
"TaskExtractMediaSegmentsDescription": "يستخرج أو يحصل على مقاطع المحتوى من الملحقات المفعّلة لمقاطع المحتوى (MediaSegment).",
"TaskMoveTrickplayImages": "نقل موقع صور معاينات التنقل",
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
"CleanupUserDataTaskDescription": "ينظف جميع بيانات المستخدم (مثل حالة المشاهدة وحالة المفضلة وغيرها) للمحتوى الذي لم يعد موجوداً لمدة 90 يوماً على الأقل.",
"Original": "فريد",
"LyricDownloadFailureFromForItem": "فشل تحميل الكلمات من {0} إلى {1}"
"LyricDownloadFailureFromForItem": "فشل تحميل الكلمات من {0} إلى {1}",
"NameExtraBehindTheScenes": "خلف المشاهد",
"NameExtraClip": "مقطع",
"NameExtraDeletedScene": "المشهد المحذوف",
"NameExtraInterview": "مقابلة",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "عيّنة",
"NameExtraScene": "مشهد",
"NameExtraShort": "قصير",
"NameExtraThemeSong": "الاغنية السمة",
"NameExtraThemeVideo": "الفيديو السمة",
"NameExtraFeaturette": "فيلم قصير إضافي",
"NameExtraTrailer": "إعلان ترويجي",
"NameExtraUnknown": "إضافي"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.",
"CleanupUserDataTask": "Tasca de neteja de dades d'usuari",
"Original": "Original",
"LyricDownloadFailureFromForItem": "No s'han pogut descarregar les lletres des de {0} per a {1}"
"LyricDownloadFailureFromForItem": "No s'han pogut descarregar les lletres des de {0} per a {1}",
"NameExtraBehindTheScenes": "Rere les càmeres",
"NameExtraClip": "Tall",
"NameExtraDeletedScene": "Escena eliminada",
"NameExtraFeaturette": "Migmetratge",
"NameExtraInterview": "Entrevista",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Mostra",
"NameExtraScene": "Escena",
"NameExtraShort": "Curt",
"NameExtraThemeSong": "Tema musical",
"NameExtraThemeVideo": "Vídeo temàtic",
"NameExtraTrailer": "Tràiler",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,16 @@
"CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten",
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Abspielstatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind.",
"Original": "Original",
"LyricDownloadFailureFromForItem": "Fehler beim Download der Songtexte von {0} für {1}"
"LyricDownloadFailureFromForItem": "Fehler beim Download der Songtexte von {0} für {1}",
"NameExtraBehindTheScenes": "Behind The Scenes",
"NameExtraDeletedScene": "Entfernte Szene",
"NameExtraInterview": "Interview",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Ausschnitt",
"NameExtraScene": "Szene",
"NameExtraShort": "Kurzfilm",
"NameExtraThemeSong": "Titellied",
"NameExtraThemeVideo": "Titelvideo",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, estado de los favoritos, etc.) que no están presentes en la biblioteca por al menos 90 días.",
"CleanupUserDataTask": "Tarea de limpieza de datos de usuarios",
"LyricDownloadFailureFromForItem": "No se pudo descargar la letra desde {0} para {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Detrás de cámaras",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Escena eliminada",
"NameExtraFeaturette": "Minidocumental",
"NameExtraInterview": "Entrevista",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Muestra",
"NameExtraScene": "Escena",
"NameExtraShort": "Cortometraje",
"NameExtraThemeSong": "Música de presentación",
"NameExtraThemeVideo": "Video de presentación",
"NameExtraTrailer": "Tráiler",
"NameExtraUnknown": "Extra"
}
@@ -106,5 +106,20 @@
"TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
"TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
"CleanupUserDataTask": "Tarea de limpieza de los datos del usuario",
"CleanupUserDataTaskDescription": "Limpia toda la información de usuario (Estado de última vez visto, favoritos, etc) del archivo media que no está presente por los últimos 90 días."
"CleanupUserDataTaskDescription": "Limpia toda la información de usuario (Estado de última vez visto, favoritos, etc) del archivo media que no está presente por los últimos 90 días.",
"LyricDownloadFailureFromForItem": "No se pudo descargar la letra desde {0} para {1}",
"NameExtraBehindTheScenes": "Detrás de cámaras",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Escena eliminada",
"NameExtraFeaturette": "Minidocumental",
"NameExtraInterview": "Entrevista",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Muestra",
"NameExtraScene": "Escena",
"NameExtraShort": "Cortometraje",
"NameExtraThemeSong": "Música de presentación",
"NameExtraThemeVideo": "Video de presentación",
"NameExtraTrailer": "Tráiler",
"NameExtraUnknown": "Extra",
"Original": "Original"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Tarea de limpieza de datos de usuario",
"CleanupUserDataTaskDescription": "Limpia todos los datos de usuario (estado de visualización, favoritos, etc.) que no están presentes en la biblioteca por al menos 90 días.",
"LyricDownloadFailureFromForItem": "No se pudo descargar las letras de {0} para {1}",
"Original": "Original"
"Original": "Original",
"NameExtraUnknown": "Extra",
"NameExtraBehindTheScenes": "Detrás de cámaras",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Escena eliminada",
"NameExtraFeaturette": "Minidocumental",
"NameExtraInterview": "Entrevista",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Muestra",
"NameExtraScene": "Escena",
"NameExtraShort": "Cortometraje",
"NameExtraThemeSong": "Música de presentación",
"NameExtraThemeVideo": "Video de presentación",
"NameExtraTrailer": "Tráiler"
}
@@ -101,5 +101,13 @@
"NotificationOptionAudioPlaybackStopped": "Ljóðspæl steðgað",
"TaskExtractMediaSegments": "Leita eftir margmiðlabrotum",
"TaskExtractMediaSegmentsDescription": "Framleiður upplýsingar um brot í margmiðlum, við hjálp frá MediaSegment-virktum ískoytisforritum.",
"NotificationOptionCameraImageUploaded": "Ljósmynd uppsent"
"NotificationOptionCameraImageUploaded": "Ljósmynd uppsend",
"NameExtraShort": "Stuttfilmur",
"NameExtraThemeSong": "Eyðkennislag",
"NameExtraTrailer": "Forfilmur",
"NameExtraInterview": "Samrøða",
"NameExtraBehindTheScenes": "Aftanfyri leiktjøldini",
"NameExtraClip": "Klipp",
"NameExtraNumbered": "{0} {1}",
"NameExtraFeaturette": "Stuttur heimildarfilmur"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
"LyricDownloadFailureFromForItem": "Le téléchargement des paroles a échoué de {0} pour {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Dans Les Coulisses",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Scène supprimée",
"NameExtraFeaturette": "Court-métrage",
"NameExtraInterview": "Entrevue",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Échantillon",
"NameExtraScene": "Scène",
"NameExtraShort": "Court-métrage",
"NameExtraThemeSong": "Chanson thème",
"NameExtraThemeVideo": "Générique",
"NameExtraTrailer": "Bande-annonce",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur",
"LyricDownloadFailureFromForItem": "Le téléchargement des paroles à échoué de {0} pour {1}",
"Original": "Original"
"Original": "Original",
"NameExtraBehindTheScenes": "Dans Les Coulisses",
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Scène supprimée",
"NameExtraFeaturette": "Court-métrage",
"NameExtraInterview": "Entrevue",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Échantillon",
"NameExtraScene": "Scène",
"NameExtraShort": "Court-métrage",
"NameExtraThemeSong": "Thème musical",
"NameExtraThemeVideo": "Générique",
"NameExtraTrailer": "Bande-annonce",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,18 @@
"Original": "Upprunaleg",
"TaskExtractMediaSegmentsDescription": "Sækir myndbúta úr viðbótum þar sem MediaSegment er virkt.",
"TaskMoveTrickplayImages": "Flytja geymslustað fyrir Trickplay-myndir",
"TaskMoveTrickplayImagesDescription": "Flytur fyrirliggjandi Trickplay-skrár í samræmi við stillingar safnsins."
"TaskMoveTrickplayImagesDescription": "Flytur fyrirliggjandi Trickplay-skrár í samræmi við stillingar safnsins.",
"NameExtraBehindTheScenes": "Bak við tjöldin",
"NameExtraClip": "Brot",
"NameExtraDeletedScene": "Eydd atriði",
"NameExtraFeaturette": "Stutt heimildarmynd",
"NameExtraInterview": "Viðtal",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Sýnishorn",
"NameExtraScene": "Sena",
"NameExtraShort": "Stuttmynd",
"NameExtraThemeSong": "Þema lag",
"NameExtraThemeVideo": "Þema myndband",
"NameExtraTrailer": "Stikla",
"NameExtraUnknown": "Aukaefni"
}
@@ -108,5 +108,15 @@
"CleanupUserDataTask": "Task di pulizia dei dati utente",
"CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni.",
"Original": "Originale",
"LyricDownloadFailureFromForItem": "Scaricamento dei testi non riuscito da {0} per {1}"
"LyricDownloadFailureFromForItem": "Scaricamento dei testi non riuscito da {0} per {1}",
"NameExtraBehindTheScenes": "Dietro le scene",
"NameExtraClip": "Filmato",
"NameExtraDeletedScene": "Scena eliminata",
"NameExtraInterview": "Intervista",
"NameExtraNumbered": "{0} {1}",
"NameExtraScene": "Scena",
"NameExtraSample": "Campione",
"NameExtraShort": "Corto",
"NameExtraThemeSong": "Sigla musicale",
"NameExtraTrailer": "Trailer"
}
@@ -108,5 +108,8 @@
"CleanupUserDataTask": "사용자 데이터 정리 작업",
"CleanupUserDataTaskDescription": "최소 90일 이상 존재하지 않는 미디어에 대한 사용자 데이터(시청 상태, 즐겨찾기 등)를 정리합니다.",
"LyricDownloadFailureFromForItem": "{1}에 대한 가사를 {0}에서 다운로드하지 못했습니다",
"Original": "원본"
"Original": "원본",
"NameExtraClip": "클립",
"NameExtraDeletedScene": "삭제된 장면",
"NameExtraInterview": "인터뷰"
}
@@ -106,5 +106,19 @@
"TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.",
"TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius",
"CleanupUserDataTask": "Naudotojo duomenų valymo užduotis",
"CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamiausią būseną ir t. t.)."
"CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamiausią būseną ir t. t.).",
"LyricDownloadFailureFromForItem": "Nepavyko atsisiųsti dainos žodžių iš {0}, skirto {1}",
"NameExtraBehindTheScenes": "Užkulisiuose",
"NameExtraClip": "Klipas",
"NameExtraDeletedScene": "Ištrinta scena",
"NameExtraInterview": "Interviu",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Pavyzdys",
"NameExtraScene": "Scena",
"NameExtraThemeSong": "Teminė daina",
"NameExtraThemeVideo": "Teminis vaizdo įrašas",
"NameExtraTrailer": "Anonsas",
"NameExtraUnknown": "Papildomas",
"Original": "Originalus",
"NameExtraFeaturette": "Trumpametražis filmas"
}
@@ -110,5 +110,16 @@
"Original": "Oorspronkelijk",
"LyricDownloadFailureFromForItem": "Downloaden van liedteksten voor {1} van {0} mislukt",
"NameExtraBehindTheScenes": "Achter de schermen",
"NameExtraClip": "Clip"
"NameExtraClip": "Clip",
"NameExtraDeletedScene": "Geschrapte scène",
"NameExtraFeaturette": "Featurette",
"NameExtraInterview": "Vraaggesprek",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Voorbeeldfragment",
"NameExtraScene": "Scène",
"NameExtraShort": "Korte film",
"NameExtraThemeSong": "Themamuziek",
"NameExtraThemeVideo": "Themavideo",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Extra inhoud"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Prečistiť používateľské dáta",
"CleanupUserDataTaskDescription": "Vyčistí všetky dáta používateľa (stav sledovania, stav obľúbených atď.) z médií, ktoré už neexistujú aspoň 90 dní.",
"LyricDownloadFailureFromForItem": "Text piesne sa nepodarilo stiahnuť z {0} pre {1}",
"Original": "Originál"
"Original": "Originál",
"NameExtraBehindTheScenes": "Zo zákulisia",
"NameExtraClip": "Klip",
"NameExtraDeletedScene": "Vystrihnutá scéna",
"NameExtraFeaturette": "Bonus",
"NameExtraInterview": "Rozhovor",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Ukážka",
"NameExtraScene": "Scéna",
"NameExtraShort": "Krátky film",
"NameExtraThemeSong": "Úvodná pieseň",
"NameExtraThemeVideo": "Úvodné video",
"NameExtraTrailer": "Trailer",
"NameExtraUnknown": "Extra"
}
@@ -108,5 +108,13 @@
"CleanupUserDataTaskDescription": "Tar bort all användardata (såsom vad du sett, favoriter med mera) för media som inte funnits på enheten på minst 90 dagar.",
"CleanupUserDataTask": "Uppgift för rensning av användardata",
"Original": "Original",
"LyricDownloadFailureFromForItem": "Misslyckades att ladda ner låttexter från {0} för {1}"
"LyricDownloadFailureFromForItem": "Misslyckades att ladda ner låttexter från {0} för {1}",
"NameExtraBehindTheScenes": "Bakom kulisserna",
"NameExtraDeletedScene": "Borttagen scen",
"NameExtraInterview": "Intervju",
"NameExtraNumbered": "{0} {1}",
"NameExtraScene": "Scen",
"NameExtraShort": "Kortfilm",
"NameExtraThemeSong": "Signaturmelodi",
"NameExtraTrailer": "Trailer"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "Завдання очищення даних користувача",
"CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому.",
"Original": "Оригінал",
"LyricDownloadFailureFromForItem": "Не вдалося завантажити текст пісні з {0} для {1}"
"LyricDownloadFailureFromForItem": "Не вдалося завантажити текст пісні з {0} для {1}",
"NameExtraBehindTheScenes": "За лаштунками",
"NameExtraClip": "Кліп",
"NameExtraDeletedScene": "Видалена сцена",
"NameExtraFeaturette": "Фічуретка",
"NameExtraInterview": "Інтерв’ю",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "Приклад",
"NameExtraScene": "Сцена",
"NameExtraShort": "Коротко",
"NameExtraThemeSong": "Тематична пісня",
"NameExtraThemeVideo": "Тематичне вiдео",
"NameExtraTrailer": "Трейлер",
"NameExtraUnknown": "Додатково"
}
@@ -108,5 +108,18 @@
"CleanupUserDataTask": "用户数据清理任务",
"CleanupUserDataTaskDescription": "清理已被删除超过90天的媒体中的所有用户数据(观看状态、收藏夹状态等)。",
"LyricDownloadFailureFromForItem": "无法从 {0} 下载 {1} 的歌词",
"Original": "原始"
"Original": "原始",
"NameExtraBehindTheScenes": "幕后花絮",
"NameExtraClip": "片段",
"NameExtraDeletedScene": "删减场景",
"NameExtraFeaturette": "花絮",
"NameExtraInterview": "采访",
"NameExtraNumbered": "{0} {1}",
"NameExtraSample": "样本",
"NameExtraScene": "场景",
"NameExtraShort": "短片",
"NameExtraThemeSong": "主题曲",
"NameExtraThemeVideo": "主题视频",
"NameExtraTrailer": "预告片",
"NameExtraUnknown": "额外"
}
@@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.Session
if (inactive.Count > 0)
{
_logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
_logger.LogDebug("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
}
foreach (var webSocket in inactive)
@@ -499,16 +499,21 @@ public sealed partial class BaseItemRepository
var inProgress = context.UserData
.Where(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0);
// Folders are resumable when a descendant is in progress, or when they hold both played and
// unplayed descendants (partially watched). Alternate versions keep their own progress, so
// they count towards the in-progress check but not towards the played/unplayed one.
// Series and Seasons are resumable when a descendant is in progress, or when they hold both
// played and unplayed descendants (partially watched). Alternate versions keep their own
// progress, so they count towards the in-progress check but not towards the played/unplayed one.
var leafItems = GetAccessFilteredLeafItemsQuery(context, filter.User!);
var inProgressLeafItems = GetAccessFilteredLeafItemsQuery(context, filter.User!, includeOwnedItems: true)
.Where(e => e.UserData!.Any(ud => ud.UserId == userId && ud.PlaybackPositionTicks > 0));
var folderResumableFilter = BuildHasDescendantFilter(context, inProgressLeafItems)
.Or(BuildHasDescendantFilter(context, leafItems.Where(e => e.UserData!.Any(ud => ud.UserId == userId && ud.Played)))
.And(BuildHasDescendantFilter(context, leafItems.Where(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played)))));
// Every other folder kind is a container rather than one continuous piece of media
var resumableFolderTypes = _resumableFolderKinds
.Select(kind => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(kind))
.ToArray();
var folderIsResumableFilter = IsFolderFilter.And(e => resumableFolderTypes.Contains(e.Type))
.And(BuildHasDescendantFilter(context, inProgressLeafItems)
.Or(BuildHasDescendantFilter(context, leafItems.Where(e => e.UserData!.Any(ud => ud.UserId == userId && ud.Played)))
.And(BuildHasDescendantFilter(context, leafItems.Where(e => !e.UserData!.Any(ud => ud.UserId == userId && ud.Played))))));
if (isResumable)
{
@@ -516,7 +521,7 @@ public sealed partial class BaseItemRepository
// Match each version on its own progress rather than coalescing onto the primary.
var inProgressIds = inProgress.Select(ud => ud.ItemId);
baseQuery = baseQuery.Where(IsFolderFilter.And(folderResumableFilter)
baseQuery = baseQuery.Where(folderIsResumableFilter
.Or(IsFolderFilter.Not().And(e => inProgressIds.Contains(e.Id))));
// When several versions of the same item are in progress, keep only the most recently played one, use id as tiebreaker.
@@ -543,7 +548,7 @@ public sealed partial class BaseItemRepository
var resumableMovieIds = inProgress
.Join(context.BaseItems, ud => ud.ItemId, bi => bi.Id, (ud, bi) => bi.PrimaryVersionId ?? bi.Id);
baseQuery = baseQuery.Where(IsFolderFilter.And(folderResumableFilter.Not())
baseQuery = baseQuery.Where(IsFolderFilter.And(folderIsResumableFilter.Not())
.Or(IsFolderFilter.Not().And(e => !resumableMovieIds.Contains(e.Id))));
}
}
@@ -63,6 +63,14 @@ public sealed partial class BaseItemRepository
(BaseItemKind.Studio, _getStudiosValueTypes)
];
// The only folder kinds whose children form a single viewing sequence, so playback progress on a
// child rolls up to them. Every other folder kind is a container that cannot be resumed.
private static readonly BaseItemKind[] _resumableFolderKinds =
[
BaseItemKind.Series,
BaseItemKind.Season
];
/// <summary>
/// Initializes a new instance of the <see cref="BaseItemRepository"/> class.
/// </summary>