Compare commits

...

38 Commits

Author SHA1 Message Date
Jellyfin Release Bot
b25d6d1e48 Bump version to 10.9.6 2024-06-06 14:41:10 -04:00
Bond-009
cf59140276 Merge pull request #11959 from Shadowghost/continue-validation-if-removed
Do not stop validation if folder was removed
2024-06-06 20:02:38 +02:00
Bond-009
cc4563a477 Use only 1 write connection/DB (#11986) 2024-06-06 08:04:51 -06:00
gnattu
0d984b5162 Fix fallback artist when taglib fails (#11989) 2024-06-06 08:04:33 -06:00
Tim Eisele
279cba008b Set ProductionLocations instead of Tags (#11984) 2024-06-06 06:47:15 -06:00
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
0359035000 Do not stop validation if folder was removed 2024-06-04 23:14:45 +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
32 changed files with 398 additions and 140 deletions

View File

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

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Jellyfin.Extensions;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
@@ -13,6 +14,8 @@ namespace Emby.Server.Implementations.Data
public abstract class BaseSqliteRepository : IDisposable
{
private bool _disposed = false;
private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
private SqliteConnection _writeConnection;
/// <summary>
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
@@ -98,9 +101,55 @@ namespace Emby.Server.Implementations.Data
}
}
protected SqliteConnection GetConnection()
protected ManagedConnection GetConnection(bool readOnly = false)
{
var connection = new SqliteConnection($"Filename={DbFilePath}");
if (!readOnly)
{
_writeLock.Wait();
if (_writeConnection is not null)
{
return new ManagedConnection(_writeConnection, _writeLock);
}
var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False");
writeConnection.Open();
if (CacheSize.HasValue)
{
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
}
if (!string.IsNullOrWhiteSpace(LockingMode))
{
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
}
if (!string.IsNullOrWhiteSpace(JournalMode))
{
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
}
if (JournalSizeLimit.HasValue)
{
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
}
if (Synchronous.HasValue)
{
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
if (PageSize.HasValue)
{
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
}
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
return new ManagedConnection(_writeConnection = writeConnection, _writeLock);
}
var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly");
connection.Open();
if (CacheSize.HasValue)
@@ -135,17 +184,17 @@ namespace Emby.Server.Implementations.Data
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
return connection;
return new ManagedConnection(connection, null);
}
public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
{
var command = connection.CreateCommand();
command.CommandText = sql;
return command;
}
protected bool TableExists(SqliteConnection connection, string name)
protected bool TableExists(ManagedConnection connection, string name)
{
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
foreach (var row in statement.ExecuteQuery())
@@ -159,7 +208,7 @@ namespace Emby.Server.Implementations.Data
return false;
}
protected List<string> GetColumnNames(SqliteConnection connection, string table)
protected List<string> GetColumnNames(ManagedConnection connection, string table)
{
var columnNames = new List<string>();
@@ -174,7 +223,7 @@ namespace Emby.Server.Implementations.Data
return columnNames;
}
protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
{
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{
@@ -207,6 +256,24 @@ namespace Emby.Server.Implementations.Data
return;
}
if (dispose)
{
_writeLock.Wait();
try
{
_writeConnection.Dispose();
}
finally
{
_writeLock.Release();
}
_writeLock.Dispose();
}
_writeConnection = null;
_writeLock = null;
_disposed = true;
}
}

View File

@@ -0,0 +1,62 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Data.Sqlite;
namespace Emby.Server.Implementations.Data;
public sealed class ManagedConnection : IDisposable
{
private readonly SemaphoreSlim? _writeLock;
private SqliteConnection _db;
private bool _disposed = false;
public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock)
{
_db = db;
_writeLock = writeLock;
}
public SqliteTransaction BeginTransaction()
=> _db.BeginTransaction();
public SqliteCommand CreateCommand()
=> _db.CreateCommand();
public void Execute(string commandText)
=> _db.Execute(commandText);
public SqliteCommand PrepareStatement(string sql)
=> _db.PrepareStatement(sql);
public IEnumerable<SqliteDataReader> Query(string commandText)
=> _db.Query(commandText);
public void Dispose()
{
if (_disposed)
{
return;
}
if (_writeLock is null)
{
// Read connections are managed with an internal pool
_db.Dispose();
}
else
{
// Write lock is managed by BaseSqliteRepository
// Don't dispose here
_writeLock.Release();
}
_db = null!;
_disposed = true;
}
}

View File

@@ -601,7 +601,7 @@ namespace Emby.Server.Implementations.Data
transaction.Commit();
}
private void SaveItemsInTransaction(SqliteConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
{
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
@@ -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);
@@ -1980,7 +1980,7 @@ namespace Emby.Server.Implementations.Data
transaction.Commit();
}
private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, SqliteConnection db)
private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, ManagedConnection db)
{
var startIndex = 0;
var limit = 100;
@@ -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))
@@ -4476,7 +4476,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
transaction.Commit();
}
private void ExecuteWithSingleParam(SqliteConnection db, string query, Guid value)
private void ExecuteWithSingleParam(ManagedConnection db, string query, Guid value)
{
using (var statement = PrepareStatement(db, 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
@@ -4632,7 +4632,7 @@ AND Type = @InternalPersonType)");
return whereClauses;
}
private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement)
private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, ManagedConnection db, SqliteCommand deleteAncestorsStatement)
{
if (itemId.IsEmpty())
{
@@ -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)
{
@@ -5148,7 +5148,7 @@ AND Type = @InternalPersonType)");
return list;
}
private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db)
private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, ManagedConnection db)
{
if (itemId.IsEmpty())
{
@@ -5167,7 +5167,7 @@ AND Type = @InternalPersonType)");
InsertItemValues(itemId, values, db);
}
private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, SqliteConnection db)
private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, ManagedConnection db)
{
const int Limit = 100;
var startIndex = 0;
@@ -5239,7 +5239,7 @@ AND Type = @InternalPersonType)");
transaction.Commit();
}
private void InsertPeople(Guid id, List<PersonInfo> people, SqliteConnection db)
private void InsertPeople(Guid id, List<PersonInfo> people, ManagedConnection db)
{
const int Limit = 100;
var startIndex = 0;
@@ -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>();
@@ -5388,7 +5388,7 @@ AND Type = @InternalPersonType)");
transaction.Commit();
}
private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, SqliteConnection db)
private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, ManagedConnection db)
{
const int Limit = 10;
var startIndex = 0;
@@ -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);
@@ -5772,7 +5772,7 @@ AND Type = @InternalPersonType)");
private void InsertMediaAttachments(
Guid id,
IReadOnlyList<MediaAttachment> attachments,
SqliteConnection db,
ManagedConnection db,
CancellationToken cancellationToken)
{
const int InsertAtOnce = 10;

View File

@@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Data
}
}
private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
{
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Data
}
}
private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
{
var list = new List<Guid>();
@@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.Data
}
}
private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
{
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
{
@@ -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.6</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.6</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -364,7 +364,7 @@ namespace MediaBrowser.Controller.Entities
if (IsFileProtocol)
{
IEnumerable<BaseItem> nonCachedChildren;
IEnumerable<BaseItem> nonCachedChildren = [];
try
{
@@ -373,7 +373,6 @@ namespace MediaBrowser.Controller.Entities
catch (Exception ex)
{
Logger.LogError(ex, "Error retrieving children folder");
return;
}
progress.Report(ProgressHelpers.RetrievedChildren);

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.6</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.6</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);
}
}
}
@@ -1050,7 +1057,7 @@ namespace MediaBrowser.Providers.Manager
}
else
{
target.Tags = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
}
}
@@ -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

@@ -193,11 +193,11 @@ namespace MediaBrowser.Providers.MediaInfo
}
tags ??= new TagLib.Id3v2.Tag();
tags.AlbumArtists ??= mediaInfo.AlbumArtists;
tags.AlbumArtists = tags.AlbumArtists.Length == 0 ? mediaInfo.AlbumArtists : tags.AlbumArtists;
tags.Album ??= mediaInfo.Album;
tags.Title ??= mediaInfo.Name;
tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year;
tags.Performers ??= mediaInfo.Artists;
tags.Performers = tags.Performers.Length == 0 ? mediaInfo.Artists : tags.Performers;
tags.Genres ??= mediaInfo.Genres;
tags.Track = tags.Track == 0U ? Convert.ToUInt32(mediaInfo.IndexNumber, CultureInfo.InvariantCulture) : tags.Track;
tags.Disc = tags.Disc == 0U ? Convert.ToUInt32(mediaInfo.ParentIndexNumber, CultureInfo.InvariantCulture) : tags.Disc;

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.6")]
[assembly: AssemblyFileVersion("10.9.6")]

View File

@@ -15,7 +15,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Extensions</PackageId>
<VersionPrefix>10.9.4</VersionPrefix>
<VersionPrefix>10.9.6</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);
}
}
}