Compare commits

..

32 Commits

Author SHA1 Message Date
Jellyfin Release Bot
2b5d458456 Bump version to 10.9.5 2024-06-05 18:04:17 -04:00
Joshua M. Boniface
f41efb3b2c Merge pull request #11978 from Shadowghost/do-to-fallback-to-media-dir
Fallback to local dir when saving to media dir fails
2024-06-05 17:49:57 -04:00
Shadowghost
0155293c64 Fallback to local dir when saving to media dir fails 2024-06-05 23:39:13 +02:00
Joshua M. Boniface
b78efd6b1e Merge pull request #11963 from gnattu/fix-rename-lib
Fix Library renaming
2024-06-05 17:30:56 -04:00
Joshua M. Boniface
bfcc09db8a Merge pull request #11921 from Shadowghost/fix-identify-over-nfo
Fix identify over NFO and replace all when NFO saving enabled
2024-06-05 17:24:36 -04:00
Joshua M. Boniface
a46c17e19f Merge pull request #11969 from Bond-009/readconns
Create readonly DB connections when possible
2024-06-05 17:09:34 -04:00
Tim Eisele
b0bb22b650 Fix local image saving (#11934) 2024-06-05 15:08:12 -06:00
Joshua M. Boniface
0c039145e5 Merge pull request #11935 from Shadowghost/fix-movie-nfo
Fix dateadded and movie NFO recognition
2024-06-05 17:01:17 -04:00
Joshua M. Boniface
2a3c904a9f Merge pull request #11943 from Shadowghost/increase-migration-batch-size
Increase lyrics migration batch size to 5000
2024-06-05 17:00:31 -04:00
Shadowghost
7cbdb6708b Fix windows test 2024-06-05 22:15:51 +02:00
Shadowghost
7058db2b04 Fix test 2024-06-05 20:57:19 +02:00
Shadowghost
8f7df590cd Do not replace locked fields 2024-06-05 19:16:14 +02:00
Bond_009
0f67a5ba2f Actually create readonly connection when asked 2024-06-05 10:43:40 +02:00
Bond_009
19fb00b5b7 Revert "remove readonly"
This reverts commit e7016e38b8.
2024-06-05 10:43:40 +02:00
gnattu
8683253c6d Fix Library renaming
This handler should not just spawn a normal library validation task because our new logic will prevent the removal of library root folder unless explicitly required, which will cause the old lib still "ghosting" in the db.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-06-05 08:49:33 +08:00
Shadowghost
918a36d564 Fix test 2024-06-05 01:12:47 +02:00
Shadowghost
57ae0b5796 Fix typo 2024-06-05 00:58:00 +02:00
Shadowghost
262f7dd98f Fix metadata saver check 2024-06-05 00:53:09 +02:00
Shadowghost
4714b3af67 Ignore local images when replacing and saving is enabled 2024-06-05 00:26:30 +02:00
Shadowghost
b14edb8876 Do not run local providers if replacing and saving is enabled 2024-06-04 21:54:04 +02:00
Niels van Velzen
47c5e0c2c7 Merge pull request #11958 from Shadowghost/fix-trailer-nfo-saving
Export trailer URLs in new format
2024-06-04 21:33:11 +02:00
Shadowghost
8709d94783 Export trailer URLs in new format 2024-06-04 19:28:18 +02:00
Tim Eisele
23b1251393 Do not delete file locations for virtual episodes and seasons (#11954) 2024-06-04 07:09:20 -06:00
cptn
484aea1cdb NextUp query respects Limit (#11956) 2024-06-04 07:05:32 -06:00
Niels van Velzen
d1c00ba4ed Merge pull request #11920 from Shadowghost/fix-season-path
Only set season path if season folder parsing was successful
2024-06-04 12:48:51 +02:00
Niels van Velzen
b1a5fe2f55 Merge pull request #11933 from Shadowghost/fix-trailer-duplication
Check trailer distinction by URL
2024-06-04 12:04:39 +02:00
Shadowghost
d7ff6d023c Apply review suggestions 2024-06-03 16:39:22 +02:00
Shadowghost
253e95dcba Increase lyrics migration fetch batch size to 5000 2024-06-03 13:53:34 +02:00
Shadowghost
c7ce1aa4c7 Fix dateadded and movie NFO recognition 2024-06-02 22:33:02 +02:00
Shadowghost
3d87885577 Check trailer distinction by URL 2024-06-02 21:31:31 +02:00
Shadowghost
a7e2271845 Skip local metadata providers when identifying 2024-06-02 09:17:43 +02:00
Shadowghost
2a02abee46 Only set season path if season folder parsing was successful 2024-06-02 08:59:41 +02:00
29 changed files with 248 additions and 118 deletions

View File

@@ -36,7 +36,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.9.4</VersionPrefix>
<VersionPrefix>10.9.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -98,9 +98,9 @@ namespace Emby.Server.Implementations.Data
}
}
protected SqliteConnection GetConnection()
protected SqliteConnection GetConnection(bool readOnly = false)
{
var connection = new SqliteConnection($"Filename={DbFilePath}");
var connection = new SqliteConnection($"Filename={DbFilePath}" + (readOnly ? ";Mode=ReadOnly" : string.Empty));
connection.Open();
if (CacheSize.HasValue)

View File

@@ -1261,7 +1261,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
using (var connection = GetConnection())
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
{
statement.TryBind("@guid", id);
@@ -1887,7 +1887,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
var chapters = new List<ChapterInfo>();
using (var connection = GetConnection())
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
{
statement.TryBind("@ItemId", item.Id);
@@ -1906,7 +1906,7 @@ namespace Emby.Server.Implementations.Data
{
CheckDisposed();
using (var connection = GetConnection())
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
{
statement.TryBind("@ItemId", item.Id);
@@ -2469,7 +2469,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection())
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -2537,7 +2537,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
var items = new List<BaseItem>();
using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection())
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -2745,7 +2745,7 @@ namespace Emby.Server.Implementations.Data
var list = new List<BaseItem>();
var result = new QueryResult<BaseItem>();
using var connection = GetConnection();
using var connection = GetConnection(true);
using var transaction = connection.BeginTransaction();
if (!isReturningZeroItems)
{
@@ -2927,7 +2927,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
var list = new List<Guid>();
using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection())
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -4509,7 +4509,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
var list = new List<string>();
using (var connection = GetConnection())
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
@@ -4547,7 +4547,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
var list = new List<PersonInfo>();
using (var connection = GetConnection())
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
@@ -4787,7 +4787,7 @@ AND Type = @InternalPersonType)");
var list = new List<string>();
using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection())
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
foreach (var row in statement.ExecuteQuery())
@@ -4987,8 +4987,8 @@ AND Type = @InternalPersonType)");
var list = new List<(BaseItem, ItemCounts)>();
var result = new QueryResult<(BaseItem, ItemCounts)>();
using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection())
using (var transaction = connection.BeginTransaction(deferred: true))
using (var connection = GetConnection(true))
using (var transaction = connection.BeginTransaction())
{
if (!isReturningZeroItems)
{
@@ -5335,7 +5335,7 @@ AND Type = @InternalPersonType)");
cmdText += " order by StreamIndex ASC";
using (var connection = GetConnection())
using (var connection = GetConnection(true))
{
var list = new List<MediaStream>();
@@ -5722,7 +5722,7 @@ AND Type = @InternalPersonType)");
cmdText += " order by AttachmentIndex ASC";
var list = new List<MediaAttachment>();
using (var connection = GetConnection())
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, cmdText))
{
statement.TryBind("@ItemId", query.ItemId);

View File

@@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.Data
ArgumentException.ThrowIfNullOrEmpty(key);
using (var connection = GetConnection())
using (var connection = GetConnection(true))
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{

View File

@@ -1029,7 +1029,7 @@ namespace Emby.Server.Implementations.Library
}
}
private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
{
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);

View File

@@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
IndexNumber = seasonParserResult.SeasonNumber,
SeriesId = series.Id,
SeriesName = series.Name,
Path = seasonParserResult.IsSeasonFolder ? path : args.Parent.Path
Path = seasonParserResult.IsSeasonFolder ? path : null
};
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)

View File

@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV
}
string? presentationUniqueKey = null;
int? limit = null;
int? limit = request.Limit;
if (!request.SeriesId.IsNullOrEmpty())
{
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)

View File

@@ -180,7 +180,21 @@ public class LibraryStructureController : BaseJellyfinApiController
// No need to start if scanning the library because it will handle it
if (refreshLibrary)
{
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
await _libraryManager.ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
var newLib = _libraryManager.GetUserRootFolder().Children.FirstOrDefault(f => f.Path.Equals(newPath, StringComparison.OrdinalIgnoreCase));
if (newLib is CollectionFolder folder)
{
foreach (var child in folder.GetPhysicalFolders())
{
await child.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
await child.ValidateChildren(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
}
}
else
{
// We don't know if this one can be validated individually, trigger a new validation
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
}
}
else
{

View File

@@ -18,7 +18,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.9.4</VersionPrefix>
<VersionPrefix>10.9.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -55,8 +55,9 @@ namespace Jellyfin.Server.Migrations.Routines
{
try
{
_logger.LogInformation("Backing up {Library} to {BackupPath}", DbFilename, bakPath);
File.Copy(dbPath, bakPath);
_logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
_logger.LogInformation("{Library} backed up to {BackupPath}", DbFilename, bakPath);
break;
}
catch (Exception ex)
@@ -80,7 +81,7 @@ namespace Jellyfin.Server.Migrations.Routines
{
IncludeItemTypes = [BaseItemKind.Audio],
StartIndex = startIndex,
Limit = 100,
Limit = 5000,
SkipDeserialization = true
})
.Cast<Audio>()
@@ -97,7 +98,8 @@ namespace Jellyfin.Server.Migrations.Routines
}
_itemRepository.SaveItems(results, CancellationToken.None);
startIndex += 100;
startIndex += results.Count;
_logger.LogInformation("Backfilled data for {UpdatedRecords} of {TotalRecords} audio records", startIndex, records);
}
}
}

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Common</PackageId>
<VersionPrefix>10.9.4</VersionPrefix>
<VersionPrefix>10.9.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -149,6 +149,14 @@ namespace MediaBrowser.Controller.Library
/// <returns>Task.</returns>
Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken);
/// <summary>
/// Reloads the root media folder.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="removeRoot">Is remove the library itself allowed.</param>
/// <returns>Task.</returns>
Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false);
Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false);
/// <summary>

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId>
<VersionPrefix>10.9.4</VersionPrefix>
<VersionPrefix>10.9.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -28,6 +28,22 @@ namespace MediaBrowser.Controller.Providers
return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
}
public List<FileSystemMetadata> GetDirectories(string path)
{
var list = new List<FileSystemMetadata>();
var items = GetFileSystemEntries(path);
for (var i = 0; i < items.Length; i++)
{
var item = items[i];
if (item.IsDirectory)
{
list.Add(item);
}
}
return list;
}
public List<FileSystemMetadata> GetFiles(string path)
{
var list = new List<FileSystemMetadata>();

View File

@@ -9,6 +9,8 @@ namespace MediaBrowser.Controller.Providers
{
FileSystemMetadata[] GetFileSystemEntries(string path);
List<FileSystemMetadata> GetDirectories(string path);
List<FileSystemMetadata> GetFiles(string path);
FileSystemMetadata? GetFile(string path);

View File

@@ -140,6 +140,14 @@ namespace MediaBrowser.Controller.Providers
IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
where T : BaseItem;
/// <summary>
/// Gets the metadata savers for the provided item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="libraryOptions">The library options.</param>
/// <returns>The metadata savers.</returns>
IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions);
/// <summary>
/// Gets all metadata plugins.
/// </summary>

View File

@@ -38,19 +38,28 @@ namespace MediaBrowser.LocalMetadata.Images
}
var parentPathFiles = directoryService.GetFiles(parentPath);
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan()).ToString();
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan());
var thumbName = string.Concat(nameWithoutExtension, "-thumb");
var images = GetImageFilesFromFolder(thumbName, parentPathFiles);
return GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles);
var metadataSubPath = directoryService.GetDirectories(parentPath).Where(d => d.Name.EndsWith("metadata", StringComparison.OrdinalIgnoreCase)).ToList();
foreach (var path in metadataSubPath)
{
var files = directoryService.GetFiles(path.FullName);
images.AddRange(GetImageFilesFromFolder(nameWithoutExtension, files));
}
return images;
}
private List<LocalImageInfo> GetFilesFromParentFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles)
private List<LocalImageInfo> GetImageFilesFromFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> filePaths)
{
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
var list = new List<LocalImageInfo>(1);
foreach (var i in parentPathFiles)
foreach (var i in filePaths)
{
if (i.IsDirectory)
{

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Model</PackageId>
<VersionPrefix>10.9.4</VersionPrefix>
<VersionPrefix>10.9.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -100,8 +100,8 @@ namespace MediaBrowser.Providers.Manager
{
saveLocally = false;
// If season is virtual under a physical series, save locally if using compatible convention
if (item is Season season && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible)
// If season is virtual under a physical series, save locally
if (item is Season season)
{
var series = season.Series;
@@ -126,7 +126,11 @@ namespace MediaBrowser.Providers.Manager
var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally);
var retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false);
string[] retryPaths = [];
if (saveLocally)
{
retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false);
}
// If there are more than one output paths, the stream will need to be seekable
if (paths.Length > 1 && !source.CanSeek)
@@ -183,6 +187,13 @@ namespace MediaBrowser.Providers.Manager
try
{
_fileSystem.DeleteFile(currentPath);
// Remove containing directory if empty
var folder = Path.GetDirectoryName(currentPath);
if (!_fileSystem.GetFiles(folder).Any())
{
Directory.Delete(folder);
}
}
catch (FileNotFoundException)
{
@@ -374,6 +385,45 @@ namespace MediaBrowser.Providers.Manager
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to determine image file extension from mime type {0}", mimeType));
}
if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
{
extension = ".jpg";
}
extension = extension.ToLowerInvariant();
if (type == ImageType.Primary && saveLocally)
{
if (season is not null && season.IndexNumber.HasValue)
{
var seriesFolder = season.SeriesPath;
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
: season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-poster" + extension;
return Path.Combine(seriesFolder, imageFilename);
}
}
if (type == ImageType.Backdrop && saveLocally)
{
if (season is not null && season.IndexNumber.HasValue)
{
var seriesFolder = season.SeriesPath;
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
: season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-fanart" + extension;
return Path.Combine(seriesFolder, imageFilename);
}
}
if (type == ImageType.Thumb && saveLocally)
{
if (season is not null && season.IndexNumber.HasValue)
@@ -447,20 +497,12 @@ namespace MediaBrowser.Providers.Manager
break;
}
if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
{
extension = ".jpg";
}
extension = extension.ToLowerInvariant();
string path = null;
if (saveLocally)
{
if (type == ImageType.Primary && item is Episode)
{
path = Path.Combine(Path.GetDirectoryName(item.Path), "metadata", filename + extension);
path = Path.Combine(Path.GetDirectoryName(item.Path), filename + "-thumb" + extension);
}
else if (item.IsInMixedFolder)
{

View File

@@ -371,12 +371,21 @@ namespace MediaBrowser.Providers.Manager
}
catch (FileNotFoundException)
{
// nothing to do, already gone
// Nothing to do, already gone
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Unable to delete {Image}", image.Path);
}
finally
{
// Always remove empty parent folder
var folder = Path.GetDirectoryName(image.Path);
if (Directory.Exists(folder) && !_fileSystem.GetFiles(folder).Any())
{
Directory.Delete(folder);
}
}
}
}
@@ -419,7 +428,8 @@ namespace MediaBrowser.Providers.Manager
var type = _singularImages[i];
var image = GetFirstLocalImageInfoByType(images, type);
if (image is not null)
// Only use local images if we are not replacing and saving
if (image is not null && !(item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages))
{
var currentImage = item.GetImageInfo(type, 0);
// if image file is stored with media, don't replace that later

View File

@@ -154,7 +154,8 @@ namespace MediaBrowser.Providers.Manager
id.IsAutomated = refreshOptions.IsAutomated;
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false);
var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any();
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, hasMetadataSavers, cancellationToken).ConfigureAwait(false);
updateType |= result.UpdateType;
if (result.Failures > 0)
@@ -639,6 +640,7 @@ namespace MediaBrowser.Providers.Manager
MetadataRefreshOptions options,
ICollection<IMetadataProvider> providers,
ItemImageProvider imageService,
bool isSavingMetadata,
CancellationToken cancellationToken)
{
var refreshResult = new RefreshResult
@@ -669,69 +671,74 @@ namespace MediaBrowser.Providers.Manager
temp.Item.Id = item.Id;
var foundImageTypes = new List<ImageType>();
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
// Do not execute local providers if we are identifying or replacing with local metadata saving enabled
if (options.SearchResult is null && !(isSavingMetadata && options.ReplaceAllMetadata))
{
var providerName = provider.GetType().Name;
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
var itemInfo = new ItemInfo(item);
try
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
{
var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false);
var providerName = provider.GetType().Name;
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
if (localItem.HasMetadata)
var itemInfo = new ItemInfo(item);
try
{
foreach (var remoteImage in localItem.RemoteImages)
var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false);
if (localItem.HasMetadata)
{
try
foreach (var remoteImage in localItem.RemoteImages)
{
if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
&& !options.IsReplacingImage(remoteImage.Type))
try
{
continue;
if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
&& !options.IsReplacingImage(remoteImage.Type))
{
continue;
}
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
// remember imagetype that has just been downloaded
foundImageTypes.Add(remoteImage.Type);
}
catch (HttpRequestException ex)
{
Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url);
}
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
// remember imagetype that has just been downloaded
foundImageTypes.Add(remoteImage.Type);
}
catch (HttpRequestException ex)
if (foundImageTypes.Count > 0)
{
Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url);
imageService.UpdateReplaceImages(options, foundImageTypes);
}
if (imageService.MergeImages(item, localItem.Images, options))
{
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
}
MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
break;
}
if (foundImageTypes.Count > 0)
{
imageService.UpdateReplaceImages(options, foundImageTypes);
}
if (imageService.MergeImages(item, localItem.Images, options))
{
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
}
MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
break;
Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
Logger.LogError(ex, "Error in {Provider}", provider.Name);
Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
Logger.LogError(ex, "Error in {Provider}", provider.Name);
// If a local provider fails, consider that a failure
refreshResult.ErrorMessage = ex.Message;
// If a local provider fails, consider that a failure
refreshResult.ErrorMessage = ex.Message;
}
}
}
@@ -763,7 +770,7 @@ namespace MediaBrowser.Providers.Manager
else
{
var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata;
MergeData(temp, metadata, item.LockedFields, shouldReplace, false);
MergeData(temp, metadata, item.LockedFields, shouldReplace, true);
}
}
}
@@ -1080,7 +1087,7 @@ namespace MediaBrowser.Providers.Manager
}
else
{
target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).Distinct().ToArray();
target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).DistinctBy(t => t.Url).ToArray();
}
MergeAlbumArtist(source, target, replaceData);

View File

@@ -418,6 +418,12 @@ namespace MediaBrowser.Providers.Manager
return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false);
}
/// <inheritdoc />
public IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions)
{
return _savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, ItemUpdateType.MetadataEdit, false));
}
private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata)
where T : BaseItem
{

View File

@@ -119,7 +119,8 @@ namespace MediaBrowser.Providers.TV
virtualSeason,
new DeleteOptions
{
DeleteFileLocation = true
// Internal metadata paths are removed regardless of this.
DeleteFileLocation = false
},
false);
}
@@ -176,7 +177,8 @@ namespace MediaBrowser.Providers.TV
episode,
new DeleteOptions
{
DeleteFileLocation = true
// Internal metadata paths are removed regardless of this.
DeleteFileLocation = false
},
false);
}

View File

@@ -825,7 +825,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
private string GetOutputTrailerUrl(string url)
{
// This is what xbmc expects
return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/?action=play_video&videoid=", StringComparison.OrdinalIgnoreCase);
return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/play/?video_id=", StringComparison.OrdinalIgnoreCase);
}
private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager)

View File

@@ -45,27 +45,24 @@ namespace MediaBrowser.XbmcMetadata.Savers
internal static IEnumerable<string> GetMovieSavePaths(ItemInfo item)
{
var path = item.ContainingFolderPath;
if (item.VideoType == VideoType.Dvd && !item.IsPlaceHolder)
{
var path = item.ContainingFolderPath;
yield return Path.Combine(path, "VIDEO_TS", "VIDEO_TS.nfo");
}
// only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
{
yield return Path.Combine(path, "movie.nfo");
}
if (!item.IsPlaceHolder && (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay))
{
var path = item.ContainingFolderPath;
yield return Path.Combine(path, Path.GetFileName(path) + ".nfo");
}
else
{
// only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
{
yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
}
yield return Path.ChangeExtension(item.Path, ".nfo");
}
}

View File

@@ -1,4 +1,4 @@
using System.Reflection;
[assembly: AssemblyVersion("10.9.4")]
[assembly: AssemblyFileVersion("10.9.4")]
[assembly: AssemblyVersion("10.9.5")]
[assembly: AssemblyFileVersion("10.9.5")]

View File

@@ -15,7 +15,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Extensions</PackageId>
<VersionPrefix>10.9.4</VersionPrefix>
<VersionPrefix>10.9.5</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -575,18 +575,22 @@ namespace Jellyfin.Providers.Tests.Manager
// Has to exist for querying DateModified time on file, results stored but not checked so not populating
BaseItem.FileSystem ??= Mock.Of<IFileSystem>();
var item = new Video();
var item = new Mock<Video>
{
CallBase = true
};
item.Setup(m => m.IsSaveLocalMetadataEnabled()).Returns(false);
var path = validPaths ? _testDataImagePath.Format : "invalid path {0}";
for (int i = 0; i < count; i++)
{
item.SetImagePath(type, i, new FileSystemMetadata
item.Object.SetImagePath(type, i, new FileSystemMetadata
{
FullName = string.Format(CultureInfo.InvariantCulture, path, i),
});
}
return item;
return item.Object;
}
private static ILocalImageProvider GetImageProvider(ImageType type, int count, bool validPaths)

View File

@@ -47,6 +47,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Location
var movie = new Movie() { Path = "/media/movies/Avengers Endgame", VideoType = VideoType.Dvd };
var path1 = "/media/movies/Avengers Endgame/Avengers Endgame.nfo";
var path2 = "/media/movies/Avengers Endgame/VIDEO_TS/VIDEO_TS.nfo";
var path3 = "/media/movies/Avengers Endgame/movie.nfo";
// uses ContainingFolderPath which uses Operating system specific paths
if (OperatingSystem.IsWindows())
@@ -54,12 +55,14 @@ namespace Jellyfin.XbmcMetadata.Tests.Location
movie.Path = movie.Path.Replace('/', '\\');
path1 = path1.Replace('/', '\\');
path2 = path2.Replace('/', '\\');
path3 = path3.Replace('/', '\\');
}
var paths = MovieNfoSaver.GetMovieSavePaths(new ItemInfo(movie)).ToArray();
Assert.Equal(2, paths.Length);
Assert.Equal(3, paths.Length);
Assert.Contains(path1, paths);
Assert.Contains(path2, paths);
Assert.Contains(path3, paths);
}
}
}