aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs6
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs81
-rw-r--r--Emby.Server.Implementations/Library/PathManager.cs15
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs21
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs88
-rw-r--r--Emby.Server.Implementations/Localization/Core/fo.json40
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/is.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/lv.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json4
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs18
11 files changed, 213 insertions, 70 deletions
diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index b701e7eb6d..7cae2a671b 100644
--- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -31,6 +31,12 @@ namespace Emby.Server.Implementations.Images
var includeItemTypes = DtoExtensions.GetBaseItemKindsForCollectionType(viewType);
var recursive = viewType != CollectionType.playlists;
+ if (viewType == CollectionType.music)
+ {
+ // Music albums usually don't have dedicated backdrops, so use artist instead
+ includeItemTypes = [BaseItemKind.MusicArtist];
+ }
+
return view.GetItemList(new InternalItemsQuery
{
CollapseBoxSetItems = false,
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 7591359ea4..7d0f3900c5 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -29,17 +29,41 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("String can't be empty.", nameof(attribute));
}
- var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
+ // Allow tmdb as an alias for tmdbid, tvdb for tvdbid, etc.
+ // The code below only supports aliases for attributes in the form of "<alias>id".
+ ReadOnlySpan<char> shortAttr = attribute switch
+ {
+ _ when attribute.Equals("tmdbid", StringComparison.OrdinalIgnoreCase) => "tmdb",
+ _ when attribute.Equals("tvdbid", StringComparison.OrdinalIgnoreCase) => "tvdb",
+ _ when attribute.Equals("imdbid", StringComparison.OrdinalIgnoreCase) => "imdb",
+ _ => ReadOnlySpan<char>.Empty
+ };
- // Must be at least 3 characters after the attribute =, ], any character,
- // then we offset it by 1, because we want the index and not length.
- var maxIndex = str.Length - attribute.Length - 2;
- while (attributeIndex > -1 && attributeIndex < maxIndex)
+ for (int strIndex = 0, attributeIndex = 0; attributeIndex > -1;)
{
- var attributeEnd = attributeIndex + attribute.Length;
+ // We may want to use imdbid pattern matching later, so we don't want to modify the original 'str'.
+ var subStr = str[strIndex..];
+ int attributeEnd = 0;
+
+ if (shortAttr.Length > 0)
+ {
+ // If we are using an alias it should be shorter (and a prefix), so let's search for that.
+ attributeIndex = subStr.IndexOf(shortAttr, StringComparison.OrdinalIgnoreCase);
+ attributeEnd = attributeIndex + shortAttr.Length;
+ }
+ else
+ {
+ attributeIndex = subStr.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
+ attributeEnd = attributeIndex + attribute.Length;
+ }
+
+ // The next iteration should start at the end of the attribute we just found.
+ // If attributeIndex < 0, the loop will end and strIndex won't be used again.
+ strIndex += attributeEnd;
+
if (attributeIndex > 0)
{
- var attributeOpener = str[attributeIndex - 1];
+ var attributeOpener = subStr[attributeIndex - 1];
var attributeCloser = attributeOpener switch
{
'[' => ']',
@@ -47,20 +71,37 @@ namespace Emby.Server.Implementations.Library
'{' => '}',
_ => '\0'
};
- if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
+
+ if (attributeCloser != '\0')
{
- var closingIndex = str[attributeEnd..].IndexOf(attributeCloser);
+ if (shortAttr.Length > 0
+ && attributeEnd + 1 < subStr.Length
+ && (subStr[attributeEnd] is 'i' or 'I')
+ && (subStr[attributeEnd + 1] is 'd' or 'D'))
+ {
+ // We were searching for a shortened attribute, but it's followed by "id" - let's skip it.
+ attributeEnd += 2;
+ }
- // Must be at least 1 character before the closing bracket.
- if (closingIndex > 1)
+ // attributeEnd points at '='.
+ // We need at least 1 more character and the closing bracket after that.
+ if (attributeEnd + 2 < subStr.Length && (subStr[attributeEnd] is '=' or '-'))
{
- return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
+ var closingIndex = subStr[attributeEnd..].IndexOf(attributeCloser);
+
+ // Must be at least 1 character before the closing bracket.
+ if (closingIndex > 1)
+ {
+ var trimmed = subStr[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim();
+
+ if (trimmed.Length > 0)
+ {
+ return trimmed.ToString();
+ }
+ }
}
}
}
-
- str = str[attributeEnd..];
- attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
}
// for imdbid we also accept pattern matching
@@ -70,16 +111,6 @@ namespace Emby.Server.Implementations.Library
return match ? imdbId.ToString() : null;
}
- // Allow tmdb as an alias for tmdbid
- if (attribute.Equals("tmdbid", StringComparison.OrdinalIgnoreCase))
- {
- var tmdbValue = str.GetAttributeValue("tmdb");
- if (tmdbValue is not null)
- {
- return tmdbValue;
- }
- }
-
return null;
}
diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs
index fad948ad97..2a50fcc7fe 100644
--- a/Emby.Server.Implementations/Library/PathManager.cs
+++ b/Emby.Server.Implementations/Library/PathManager.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -43,7 +44,19 @@ public class PathManager : IPathManager
public string? GetAttachmentPath(string mediaSourceId, string fileName)
{
var folder = GetAttachmentFolderPath(mediaSourceId);
- return folder is null ? null : Path.Combine(folder, fileName);
+ if (folder is null)
+ {
+ return null;
+ }
+
+ var safeName = PathHelper.GetSafeLeafFileName(fileName);
+ if (safeName is null)
+ {
+ _logger.LogWarning("Rejecting attachment filename '{FileName}' for MediaSource {MediaSourceId}: not a valid leaf name.", fileName, mediaSourceId);
+ return null;
+ }
+
+ return Path.Combine(folder, safeName);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 68b66ab7f5..80375ae12d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -376,15 +376,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
// We need to only look at the name of this actual item (not parents)
var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan());
- var tmdbid = justName.GetAttributeValue("tmdbid");
+ // The fallback filename is only used when the item isn't in a mixed folder
+ var fileName = item.IsInMixedFolder ? ReadOnlySpan<char>.Empty : Path.GetFileName(item.Path.AsSpan());
- // If not in a mixed folder and ID not found in folder path, check filename
- if (string.IsNullOrEmpty(tmdbid) && !item.IsInMixedFolder)
+ item.TrySetProviderId(MetadataProvider.Tmdb, GetIdFromNameOrPath(justName, fileName, "tmdbid"));
+ item.TrySetProviderId(MetadataProvider.Tvdb, GetIdFromNameOrPath(justName, fileName, "tvdbid"));
+
+ string GetIdFromNameOrPath(ReadOnlySpan<char> name, ReadOnlySpan<char> fallbackName, string attribute)
{
- tmdbid = Path.GetFileName(item.Path.AsSpan()).GetAttributeValue("tmdbid");
- }
+ var id = name.GetAttributeValue(attribute);
+
+ // If not in a mixed folder and ID not found in folder path, check filename
+ if (string.IsNullOrEmpty(id) && !item.IsInMixedFolder)
+ {
+ id = fallbackName.GetAttributeValue(attribute);
+ }
- item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
+ return id;
+ }
if (!string.IsNullOrEmpty(item.Path))
{
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 40cd2bb69c..f5c41e5670 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -192,7 +192,8 @@ namespace Emby.Server.Implementations.Library
}
else
{
- var userData = item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault();
+ var userDataRow = ResolveUserDataRow(item, item.UserData?.Where(e => e.UserId.Equals(user.Id)));
+ var userData = userDataRow is not null ? Map(userDataRow) : null;
if (userData is not null)
{
result[item.Id] = userData;
@@ -211,37 +212,32 @@ namespace Emby.Server.Implementations.Library
return result;
}
- // Build a single query for all missing items
+ // Build a single query for all missing items. Fetch rows by item alone so rows kept
+ // under keys from older metadata resolve the same way as the in-memory path.
var allItemIds = itemsNeedingQuery.Select(x => x.Item.Id).ToList();
- var allKeys = itemsNeedingQuery.SelectMany(x => x.Keys).Distinct().ToList();
- if (allKeys.Count > 0)
- {
- using var context = _repository.CreateDbContext();
- var userDataArray = context.UserData
- .AsNoTracking()
- .Where(e => e.UserId.Equals(user.Id))
- .WhereOneOrMany(allItemIds, e => e.ItemId)
- .WhereOneOrMany(allKeys, e => e.CustomDataKey)
- .ToArray();
-
- var userDataByItem = userDataArray.GroupBy(e => e.ItemId).ToDictionary(g => g.Key, g => g.ToArray());
- foreach (var (item, keys) in itemsNeedingQuery)
+ using var context = _repository.CreateDbContext();
+ var userDataArray = context.UserData
+ .AsNoTracking()
+ .Where(e => e.UserId.Equals(user.Id))
+ .WhereOneOrMany(allItemIds, e => e.ItemId)
+ .ToArray();
+
+ var userDataByItem = userDataArray.GroupBy(e => e.ItemId).ToDictionary(g => g.Key, g => g.ToArray());
+ foreach (var (item, keys) in itemsNeedingQuery)
+ {
+ UserItemData userData;
+ if (userDataByItem.TryGetValue(item.Id, out var itemUserData) && itemUserData.Length > 0)
{
- UserItemData userData;
- if (userDataByItem.TryGetValue(item.Id, out var itemUserData) && itemUserData.Length > 0)
- {
- var directDataReference = itemUserData.FirstOrDefault(e => e.CustomDataKey == item.Id.ToString("N"));
- userData = directDataReference is not null ? Map(directDataReference) : Map(itemUserData.First());
- }
- else
- {
- userData = new UserItemData { Key = keys.Count > 0 ? keys[0] : string.Empty };
- }
-
- result[item.Id] = userData;
- var cacheKey = GetCacheKey(user.InternalId, item.Id);
- _cache.AddOrUpdate(cacheKey, userData);
+ userData = Map(ResolveUserDataRow(item, itemUserData)!);
}
+ else
+ {
+ userData = new UserItemData { Key = keys.Count > 0 ? keys[0] : string.Empty };
+ }
+
+ result[item.Id] = userData;
+ var cacheKey = GetCacheKey(user.InternalId, item.Id);
+ _cache.AddOrUpdate(cacheKey, userData);
}
return result;
@@ -291,8 +287,8 @@ namespace Emby.Server.Implementations.Library
{
using var dbContext = _repository.CreateDbContext();
withLocalAlternates = dbContext.LinkedChildren
- .Where(lc => lc.ChildType == Jellyfin.Database.Implementations.Entities.LinkedChildType.LocalAlternateVersion
- && localProbeIds.Contains(lc.ParentId))
+ .Where(lc => lc.ChildType == Jellyfin.Database.Implementations.Entities.LinkedChildType.LocalAlternateVersion)
+ .WhereOneOrMany(localProbeIds, lc => lc.ParentId)
.Select(lc => lc.ParentId)
.Distinct()
.ToHashSet();
@@ -356,12 +352,40 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public UserItemData? GetUserData(User user, BaseItem item)
{
- return item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault() ?? new UserItemData()
+ var row = ResolveUserDataRow(item, item.UserData?.Where(e => e.UserId.Equals(user.Id)));
+ return row is not null ? Map(row) : new UserItemData()
{
Key = item.GetUserDataKeys()[0],
};
}
+ /// <summary>
+ /// Picks the row matching the item's current user data keys, in key order, so rows left behind
+ /// under keys from older metadata don't take priority over the rows the write path updates.
+ /// </summary>
+ /// <param name="item">The item whose keys to match.</param>
+ /// <param name="rows">The candidate user data rows for a single user.</param>
+ /// <returns>The best matching row, or <c>null</c> when there are none.</returns>
+ private static UserData? ResolveUserDataRow(BaseItem item, IEnumerable<UserData>? rows)
+ {
+ var candidates = rows?.ToList();
+ if (candidates is null || candidates.Count == 0)
+ {
+ return null;
+ }
+
+ foreach (var key in item.GetUserDataKeys())
+ {
+ var match = candidates.Find(e => string.Equals(e.CustomDataKey, key, StringComparison.Ordinal));
+ if (match is not null)
+ {
+ return match;
+ }
+ }
+
+ return candidates[0];
+ }
+
/// <inheritdoc />
public UserItemDataDto? GetUserDataDto(BaseItem item, User user)
=> GetUserDataDto(item, null, user, new DtoOptions());
diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json
index 4fb9f4329c..8605a752db 100644
--- a/Emby.Server.Implementations/Localization/Core/fo.json
+++ b/Emby.Server.Implementations/Localization/Core/fo.json
@@ -1,7 +1,7 @@
{
"Artists": "Listafólk",
"Collections": "Søvn",
- "Default": "Sjálvgildi",
+ "Default": "Forsett",
"External": "Ytri",
"Genres": "Greinar",
"AppDeviceValues": "App: {0}, Eind: {1}",
@@ -12,5 +12,41 @@
"Forced": "Kravt",
"FailedLoginAttemptWithUserName": "Miseydnað innritanarroynd frá {0}",
"HeaderFavoriteEpisodes": "Yndispartar",
- "LabelIpAddressValue": "IP atsetur: {0}"
+ "LabelIpAddressValue": "IP-atsetur: {0}",
+ "AuthenticationSucceededWithUserName": "{0} varð samgildur",
+ "HeaderFavoriteShows": "Yndisrøðir",
+ "HeaderLiveTV": "Beinleiðis sjónvarp",
+ "HearingImpaired": "Hoyrnarveik",
+ "Inherit": "Arvar",
+ "LabelRunningTimeValue": "Spælitíð: {0}",
+ "Latest": "Seinastu",
+ "LyricDownloadFailureFromForItem": "Miseydnaðist at niðurtakað sangtekst fyri {1} frá {0}",
+ "NameInstallFailed": "{0} innlegging miseydnaðist",
+ "NewVersionIsAvailable": "Ein nýggj útgáva av Jellyfin ambætaranum er tøk.",
+ "NotificationOptionNewLibraryContent": "Nýtt tilfar innlagt",
+ "NotificationOptionPluginInstalled": "Ískoytisforrit innlagt",
+ "NotificationOptionPluginUninstalled": "Ískoytisforrit strikað",
+ "NotificationOptionPluginUpdateInstalled": "Ískoytisforrit dagført",
+ "NotificationOptionUserLockedOut": "Brúkari útihýstur",
+ "Photos": "Ljósmyndir",
+ "PluginInstalledWithName": "{0} var innlagt",
+ "PluginUninstalledWithName": "{0} var strikað",
+ "PluginUpdatedWithName": "{0} varð dagført",
+ "Shows": "Røðir",
+ "SubtitleDownloadFailureFromForItem": "Miseydnaðist at niðurtakað undirtekstir til {1} frá {0}",
+ "TvShows": "Sjónvarpsrøðir",
+ "UserCreatedWithName": "Brúkari {0} er stovnaður",
+ "UserDeletedWithName": "Brúkari {0} er strikaður",
+ "UserDownloadingItemWithValues": "{0} niðurtekur {1}",
+ "UserLockedOutWithName": "Brúkari {0} er útihýstur",
+ "VersionNumber": "Útgáva {0}",
+ "TasksLibraryCategory": "Savn",
+ "TaskRefreshLibrary": "Skanna miðlasavn",
+ "TaskCleanLogsDescription": "Strikar gerðalistafílur eldri enn {0} dagar.",
+ "TaskUpdatePlugins": "Dagfør ískoytisforrit",
+ "TaskRefreshChannels": "Endurinnles rásir",
+ "TaskDownloadMissingLyricsDescription": "Niðurtekur sangtekstir",
+ "Movies": "Filmar",
+ "MixedContent": "Blandað innihald",
+ "Music": "Tónleikur"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 8794339fb1..442c26b30b 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -107,5 +107,6 @@
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja u postavke biblioteke.",
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana.",
- "Original": "Original"
+ "Original": "Original",
+ "LyricDownloadFailureFromForItem": "Preuzimanje tekstova pjesmi od {0} za {1} nije uspjelo"
}
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index 44e057e4de..825ad3084e 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -106,5 +106,7 @@
"CleanupUserDataTaskDescription": "Hreinsar öll notendagögn (spilunarstöðu, uppáhöld o.s.frv.) um gögn sem hafa ekki verið til staðar í að lámarki 90 daga.",
"LyricDownloadFailureFromForItem": "Ekki tókst að niðurhala texta frá {0} fyrir {1}",
"Original": "Upprunaleg",
- "TaskExtractMediaSegmentsDescription": "Sækir myndbúta úr viðbótum þar sem MediaSegment er virkt."
+ "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."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index 4a1b248e76..76fa9e3cf7 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -107,5 +107,6 @@
"TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām",
"CleanupUserDataTask": "Lietotāju datu tīrīšanas uzdevums",
"CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas.",
- "Original": "Oriģināls"
+ "Original": "Oriģināls",
+ "LyricDownloadFailureFromForItem": "Dziesmu vārdi nevarēja tikt lejupielādēti no {0} priekš {1}"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 5dace3b0b7..4e0dff87b9 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -106,5 +106,7 @@
"TaskMoveTrickplayImages": "遷移快轉縮圖位置",
"TaskMoveTrickplayImagesDescription": "根據媒體庫的設定遷移快轉縮圖的檔案。",
"CleanupUserDataTask": "用戶資料清理工作",
- "CleanupUserDataTaskDescription": "從用戶資料中清除已被刪除超過 90 天的媒體的相關資料。"
+ "CleanupUserDataTaskDescription": "從用戶資料中清除已被刪除超過 90 天的媒體的相關資料。",
+ "Original": "原作",
+ "LyricDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的歌詞"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index e7dd984ec4..0331ec39e5 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -262,6 +262,24 @@ namespace Emby.Server.Implementations.Localization
}
/// <inheritdoc />
+ public string? GetLanguageDisplayName(string language)
+ {
+ if (string.IsNullOrEmpty(language))
+ {
+ return null;
+ }
+
+ var displayName = FindLanguageInfo(language)?.DisplayName;
+ if (displayName is null)
+ {
+ return null;
+ }
+
+ // Truncate at the first delimiter to avoid cluttered display names
+ return displayName.Split([';', ','], StringSplitOptions.None)[0].Trim();
+ }
+
+ /// <inheritdoc />
public IReadOnlyList<CountryInfo> GetCountries()
{
using var stream = _assembly.GetManifestResourceStream(CountriesPath) ?? throw new InvalidOperationException($"Invalid resource path: '{CountriesPath}'");