aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs22
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs12
-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/UserDataManager.cs84
-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/LocalizationManager.cs18
8 files changed, 181 insertions, 58 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 69e23bcb63..0c1c7d3f5b 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -39,6 +39,8 @@ using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
+using Jellyfin.Data;
+using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Drawing;
using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Manager;
@@ -417,6 +419,8 @@ namespace Emby.Server.Implementations
{
Logger.LogInformation("Running startup tasks");
+ EnsureStartupWizardIntegrity();
+
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
@@ -436,6 +440,24 @@ namespace Emby.Server.Implementations
return Task.CompletedTask;
}
+ private void EnsureStartupWizardIntegrity()
+ {
+ if (ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted)
+ {
+ return;
+ }
+
+ var hasConfiguredAdministrator = Resolve<IUserManager>().GetUsers()
+ .Any(user => user.HasPermission(PermissionKind.IsAdministrator) && !string.IsNullOrEmpty(user.Password));
+
+ if (hasConfiguredAdministrator)
+ {
+ Logger.LogWarning("The startup wizard is marked incomplete but a configured administrator already exists. Marking setup as completed to prevent the unauthenticated setup endpoints from being reachable.");
+ ConfigurationManager.Configuration.IsStartupWizardCompleted = true;
+ ConfigurationManager.SaveConfiguration();
+ }
+ }
+
/// <inheritdoc/>
public void Init(IServiceCollection serviceCollection)
{
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 3691f4e19d..f684f4dca3 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -3886,5 +3886,17 @@ namespace Emby.Server.Implementations.Library
{
return _mediaStreamRepository.GetMediaStreamLanguages(mediaStreamType);
}
+
+ /// <inheritdoc />
+ public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType, InternalItemsQuery query)
+ {
+ if (query.User is not null)
+ {
+ AddUserToQuery(query, query.User);
+ }
+
+ SetTopParentOrAncestorIds(query);
+ return _itemRepository.GetMediaStreamLanguages(query, mediaStreamType);
+ }
}
}
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/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 19b42a3a11..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;
@@ -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/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/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}'");