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
+1 -1
View File
@@ -36,7 +36,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId> <PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.9.4</VersionPrefix> <VersionPrefix>10.9.6</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
@@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -13,6 +14,8 @@ namespace Emby.Server.Implementations.Data
public abstract class BaseSqliteRepository : IDisposable public abstract class BaseSqliteRepository : IDisposable
{ {
private bool _disposed = false; private bool _disposed = false;
private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
private SqliteConnection _writeConnection;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class. /// 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(); connection.Open();
if (CacheSize.HasValue) if (CacheSize.HasValue)
@@ -135,17 +184,17 @@ namespace Emby.Server.Implementations.Data
connection.Execute("PRAGMA temp_store=" + (int)TempStore); 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(); var command = connection.CreateCommand();
command.CommandText = sql; command.CommandText = sql;
return command; 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"); using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
foreach (var row in statement.ExecuteQuery()) foreach (var row in statement.ExecuteQuery())
@@ -159,7 +208,7 @@ namespace Emby.Server.Implementations.Data
return false; return false;
} }
protected List<string> GetColumnNames(SqliteConnection connection, string table) protected List<string> GetColumnNames(ManagedConnection connection, string table)
{ {
var columnNames = new List<string>(); var columnNames = new List<string>();
@@ -174,7 +223,7 @@ namespace Emby.Server.Implementations.Data
return columnNames; 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)) if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{ {
@@ -207,6 +256,24 @@ namespace Emby.Server.Implementations.Data
return; return;
} }
if (dispose)
{
_writeLock.Wait();
try
{
_writeConnection.Dispose();
}
finally
{
_writeLock.Release();
}
_writeLock.Dispose();
}
_writeConnection = null;
_writeLock = null;
_disposed = true; _disposed = true;
} }
} }
@@ -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;
}
}
@@ -601,7 +601,7 @@ namespace Emby.Server.Implementations.Data
transaction.Commit(); 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 saveItemStatement = PrepareStatement(db, SaveItemCommandText))
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId")) using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
@@ -1261,7 +1261,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed(); CheckDisposed();
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery)) using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
{ {
statement.TryBind("@guid", id); statement.TryBind("@guid", id);
@@ -1887,7 +1887,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed(); CheckDisposed();
var chapters = new List<ChapterInfo>(); 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")) using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
{ {
statement.TryBind("@ItemId", item.Id); statement.TryBind("@ItemId", item.Id);
@@ -1906,7 +1906,7 @@ namespace Emby.Server.Implementations.Data
{ {
CheckDisposed(); 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")) using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
{ {
statement.TryBind("@ItemId", item.Id); statement.TryBind("@ItemId", item.Id);
@@ -1980,7 +1980,7 @@ namespace Emby.Server.Implementations.Data
transaction.Commit(); 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 startIndex = 0;
var limit = 100; var limit = 100;
@@ -2469,7 +2469,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString(); var commandText = commandTextBuilder.ToString();
using (new QueryTimeLogger(Logger, commandText)) using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText)) using (var statement = PrepareStatement(connection, commandText))
{ {
if (EnableJoinUserData(query)) if (EnableJoinUserData(query))
@@ -2537,7 +2537,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString(); var commandText = commandTextBuilder.ToString();
var items = new List<BaseItem>(); var items = new List<BaseItem>();
using (new QueryTimeLogger(Logger, commandText)) using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText)) using (var statement = PrepareStatement(connection, commandText))
{ {
if (EnableJoinUserData(query)) if (EnableJoinUserData(query))
@@ -2745,7 +2745,7 @@ namespace Emby.Server.Implementations.Data
var list = new List<BaseItem>(); var list = new List<BaseItem>();
var result = new QueryResult<BaseItem>(); var result = new QueryResult<BaseItem>();
using var connection = GetConnection(); using var connection = GetConnection(true);
using var transaction = connection.BeginTransaction(); using var transaction = connection.BeginTransaction();
if (!isReturningZeroItems) if (!isReturningZeroItems)
{ {
@@ -2927,7 +2927,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString(); var commandText = commandTextBuilder.ToString();
var list = new List<Guid>(); var list = new List<Guid>();
using (new QueryTimeLogger(Logger, commandText)) using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText)) using (var statement = PrepareStatement(connection, commandText))
{ {
if (EnableJoinUserData(query)) if (EnableJoinUserData(query))
@@ -4476,7 +4476,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
transaction.Commit(); 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)) 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>(); var list = new List<string>();
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText.ToString())) using (var statement = PrepareStatement(connection, commandText.ToString()))
{ {
// Run this again to bind the params // 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>(); var list = new List<PersonInfo>();
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText.ToString())) using (var statement = PrepareStatement(connection, commandText.ToString()))
{ {
// Run this again to bind the params // Run this again to bind the params
@@ -4632,7 +4632,7 @@ AND Type = @InternalPersonType)");
return whereClauses; 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()) if (itemId.IsEmpty())
{ {
@@ -4787,7 +4787,7 @@ AND Type = @InternalPersonType)");
var list = new List<string>(); var list = new List<string>();
using (new QueryTimeLogger(Logger, commandText)) using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText)) using (var statement = PrepareStatement(connection, commandText))
{ {
foreach (var row in statement.ExecuteQuery()) foreach (var row in statement.ExecuteQuery())
@@ -4987,8 +4987,8 @@ AND Type = @InternalPersonType)");
var list = new List<(BaseItem, ItemCounts)>(); var list = new List<(BaseItem, ItemCounts)>();
var result = new QueryResult<(BaseItem, ItemCounts)>(); var result = new QueryResult<(BaseItem, ItemCounts)>();
using (new QueryTimeLogger(Logger, commandText)) using (new QueryTimeLogger(Logger, commandText))
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var transaction = connection.BeginTransaction(deferred: true)) using (var transaction = connection.BeginTransaction())
{ {
if (!isReturningZeroItems) if (!isReturningZeroItems)
{ {
@@ -5148,7 +5148,7 @@ AND Type = @InternalPersonType)");
return list; 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()) if (itemId.IsEmpty())
{ {
@@ -5167,7 +5167,7 @@ AND Type = @InternalPersonType)");
InsertItemValues(itemId, values, db); 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; const int Limit = 100;
var startIndex = 0; var startIndex = 0;
@@ -5239,7 +5239,7 @@ AND Type = @InternalPersonType)");
transaction.Commit(); 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; const int Limit = 100;
var startIndex = 0; var startIndex = 0;
@@ -5335,7 +5335,7 @@ AND Type = @InternalPersonType)");
cmdText += " order by StreamIndex ASC"; cmdText += " order by StreamIndex ASC";
using (var connection = GetConnection()) using (var connection = GetConnection(true))
{ {
var list = new List<MediaStream>(); var list = new List<MediaStream>();
@@ -5388,7 +5388,7 @@ AND Type = @InternalPersonType)");
transaction.Commit(); 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; const int Limit = 10;
var startIndex = 0; var startIndex = 0;
@@ -5722,7 +5722,7 @@ AND Type = @InternalPersonType)");
cmdText += " order by AttachmentIndex ASC"; cmdText += " order by AttachmentIndex ASC";
var list = new List<MediaAttachment>(); var list = new List<MediaAttachment>();
using (var connection = GetConnection()) using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, cmdText)) using (var statement = PrepareStatement(connection, cmdText))
{ {
statement.TryBind("@ItemId", query.ItemId); statement.TryBind("@ItemId", query.ItemId);
@@ -5772,7 +5772,7 @@ AND Type = @InternalPersonType)");
private void InsertMediaAttachments( private void InsertMediaAttachments(
Guid id, Guid id,
IReadOnlyList<MediaAttachment> attachments, IReadOnlyList<MediaAttachment> attachments,
SqliteConnection db, ManagedConnection db,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
const int InsertAtOnce = 10; const int InsertAtOnce = 10;
@@ -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); 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>(); 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)")) 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); 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")) using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{ {
@@ -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); await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
@@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
IndexNumber = seasonParserResult.SeasonNumber, IndexNumber = seasonParserResult.SeasonNumber,
SeriesId = series.Id, SeriesId = series.Id,
SeriesName = series.Name, SeriesName = series.Name,
Path = seasonParserResult.IsSeasonFolder ? path : args.Parent.Path Path = seasonParserResult.IsSeasonFolder ? path : null
}; };
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder) if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV
} }
string? presentationUniqueKey = null; string? presentationUniqueKey = null;
int? limit = null; int? limit = request.Limit;
if (!request.SeriesId.IsNullOrEmpty()) if (!request.SeriesId.IsNullOrEmpty())
{ {
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series) if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
@@ -180,7 +180,21 @@ public class LibraryStructureController : BaseJellyfinApiController
// No need to start if scanning the library because it will handle it // No need to start if scanning the library because it will handle it
if (refreshLibrary) 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 else
{ {
+1 -1
View File
@@ -18,7 +18,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId> <PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.9.4</VersionPrefix> <VersionPrefix>10.9.6</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
@@ -55,8 +55,9 @@ namespace Jellyfin.Server.Migrations.Routines
{ {
try try
{ {
_logger.LogInformation("Backing up {Library} to {BackupPath}", DbFilename, bakPath);
File.Copy(dbPath, bakPath); File.Copy(dbPath, bakPath);
_logger.LogInformation("Library database backed up to {BackupPath}", bakPath); _logger.LogInformation("{Library} backed up to {BackupPath}", DbFilename, bakPath);
break; break;
} }
catch (Exception ex) catch (Exception ex)
@@ -80,7 +81,7 @@ namespace Jellyfin.Server.Migrations.Routines
{ {
IncludeItemTypes = [BaseItemKind.Audio], IncludeItemTypes = [BaseItemKind.Audio],
StartIndex = startIndex, StartIndex = startIndex,
Limit = 100, Limit = 5000,
SkipDeserialization = true SkipDeserialization = true
}) })
.Cast<Audio>() .Cast<Audio>()
@@ -97,7 +98,8 @@ namespace Jellyfin.Server.Migrations.Routines
} }
_itemRepository.SaveItems(results, CancellationToken.None); _itemRepository.SaveItems(results, CancellationToken.None);
startIndex += 100; startIndex += results.Count;
_logger.LogInformation("Backfilled data for {UpdatedRecords} of {TotalRecords} audio records", startIndex, records);
} }
} }
} }
@@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Common</PackageId> <PackageId>Jellyfin.Common</PackageId>
<VersionPrefix>10.9.4</VersionPrefix> <VersionPrefix>10.9.6</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
+1 -2
View File
@@ -364,7 +364,7 @@ namespace MediaBrowser.Controller.Entities
if (IsFileProtocol) if (IsFileProtocol)
{ {
IEnumerable<BaseItem> nonCachedChildren; IEnumerable<BaseItem> nonCachedChildren = [];
try try
{ {
@@ -373,7 +373,6 @@ namespace MediaBrowser.Controller.Entities
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error retrieving children folder"); Logger.LogError(ex, "Error retrieving children folder");
return;
} }
progress.Report(ProgressHelpers.RetrievedChildren); progress.Report(ProgressHelpers.RetrievedChildren);
@@ -149,6 +149,14 @@ namespace MediaBrowser.Controller.Library
/// <returns>Task.</returns> /// <returns>Task.</returns>
Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken); 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); Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false);
/// <summary> /// <summary>
@@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId> <PackageId>Jellyfin.Controller</PackageId>
<VersionPrefix>10.9.4</VersionPrefix> <VersionPrefix>10.9.6</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
@@ -28,6 +28,22 @@ namespace MediaBrowser.Controller.Providers
return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem); 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) public List<FileSystemMetadata> GetFiles(string path)
{ {
var list = new List<FileSystemMetadata>(); var list = new List<FileSystemMetadata>();
@@ -9,6 +9,8 @@ namespace MediaBrowser.Controller.Providers
{ {
FileSystemMetadata[] GetFileSystemEntries(string path); FileSystemMetadata[] GetFileSystemEntries(string path);
List<FileSystemMetadata> GetDirectories(string path);
List<FileSystemMetadata> GetFiles(string path); List<FileSystemMetadata> GetFiles(string path);
FileSystemMetadata? GetFile(string path); FileSystemMetadata? GetFile(string path);
@@ -140,6 +140,14 @@ namespace MediaBrowser.Controller.Providers
IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions) IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
where T : BaseItem; 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> /// <summary>
/// Gets all metadata plugins. /// Gets all metadata plugins.
/// </summary> /// </summary>
@@ -38,19 +38,28 @@ namespace MediaBrowser.LocalMetadata.Images
} }
var parentPathFiles = directoryService.GetFiles(parentPath); 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 thumbName = string.Concat(filenameWithoutExtension, "-thumb");
var list = new List<LocalImageInfo>(1); var list = new List<LocalImageInfo>(1);
foreach (var i in parentPathFiles) foreach (var i in filePaths)
{ {
if (i.IsDirectory) if (i.IsDirectory)
{ {
+1 -1
View File
@@ -8,7 +8,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Model</PackageId> <PackageId>Jellyfin.Model</PackageId>
<VersionPrefix>10.9.4</VersionPrefix> <VersionPrefix>10.9.6</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
+54 -12
View File
@@ -100,8 +100,8 @@ namespace MediaBrowser.Providers.Manager
{ {
saveLocally = false; saveLocally = false;
// If season is virtual under a physical series, save locally if using compatible convention // If season is virtual under a physical series, save locally
if (item is Season season && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible) if (item is Season season)
{ {
var series = season.Series; var series = season.Series;
@@ -126,7 +126,11 @@ namespace MediaBrowser.Providers.Manager
var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally); 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 there are more than one output paths, the stream will need to be seekable
if (paths.Length > 1 && !source.CanSeek) if (paths.Length > 1 && !source.CanSeek)
@@ -183,6 +187,13 @@ namespace MediaBrowser.Providers.Manager
try try
{ {
_fileSystem.DeleteFile(currentPath); _fileSystem.DeleteFile(currentPath);
// Remove containing directory if empty
var folder = Path.GetDirectoryName(currentPath);
if (!_fileSystem.GetFiles(folder).Any())
{
Directory.Delete(folder);
}
} }
catch (FileNotFoundException) 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)); 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 (type == ImageType.Thumb && saveLocally)
{ {
if (season is not null && season.IndexNumber.HasValue) if (season is not null && season.IndexNumber.HasValue)
@@ -447,20 +497,12 @@ namespace MediaBrowser.Providers.Manager
break; break;
} }
if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
{
extension = ".jpg";
}
extension = extension.ToLowerInvariant();
string path = null; string path = null;
if (saveLocally) if (saveLocally)
{ {
if (type == ImageType.Primary && item is Episode) 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) else if (item.IsInMixedFolder)
{ {
@@ -371,12 +371,21 @@ namespace MediaBrowser.Providers.Manager
} }
catch (FileNotFoundException) catch (FileNotFoundException)
{ {
// nothing to do, already gone // Nothing to do, already gone
} }
catch (UnauthorizedAccessException ex) catch (UnauthorizedAccessException ex)
{ {
_logger.LogWarning(ex, "Unable to delete {Image}", image.Path); _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 type = _singularImages[i];
var image = GetFirstLocalImageInfoByType(images, type); 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); var currentImage = item.GetImageInfo(type, 0);
// if image file is stored with media, don't replace that later // if image file is stored with media, don't replace that later
@@ -154,7 +154,8 @@ namespace MediaBrowser.Providers.Manager
id.IsAutomated = refreshOptions.IsAutomated; 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; updateType |= result.UpdateType;
if (result.Failures > 0) if (result.Failures > 0)
@@ -639,6 +640,7 @@ namespace MediaBrowser.Providers.Manager
MetadataRefreshOptions options, MetadataRefreshOptions options,
ICollection<IMetadataProvider> providers, ICollection<IMetadataProvider> providers,
ItemImageProvider imageService, ItemImageProvider imageService,
bool isSavingMetadata,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var refreshResult = new RefreshResult var refreshResult = new RefreshResult
@@ -669,69 +671,74 @@ namespace MediaBrowser.Providers.Manager
temp.Item.Id = item.Id; temp.Item.Id = item.Id;
var foundImageTypes = new List<ImageType>(); 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; foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
var itemInfo = new ItemInfo(item);
try
{ {
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) try
&& !options.IsReplacingImage(remoteImage.Type))
{ {
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) Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
{
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;
} }
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
Logger.LogError(ex, "Error in {Provider}", provider.Name);
Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName); // If a local provider fails, consider that a failure
} refreshResult.ErrorMessage = ex.Message;
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;
} }
} }
@@ -763,7 +770,7 @@ namespace MediaBrowser.Providers.Manager
else else
{ {
var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata; 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 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 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); MergeAlbumArtist(source, target, replaceData);
@@ -418,6 +418,12 @@ namespace MediaBrowser.Providers.Manager
return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false); 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) private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata)
where T : BaseItem where T : BaseItem
{ {
@@ -193,11 +193,11 @@ namespace MediaBrowser.Providers.MediaInfo
} }
tags ??= new TagLib.Id3v2.Tag(); tags ??= new TagLib.Id3v2.Tag();
tags.AlbumArtists ??= mediaInfo.AlbumArtists; tags.AlbumArtists = tags.AlbumArtists.Length == 0 ? mediaInfo.AlbumArtists : tags.AlbumArtists;
tags.Album ??= mediaInfo.Album; tags.Album ??= mediaInfo.Album;
tags.Title ??= mediaInfo.Name; tags.Title ??= mediaInfo.Name;
tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year; 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.Genres ??= mediaInfo.Genres;
tags.Track = tags.Track == 0U ? Convert.ToUInt32(mediaInfo.IndexNumber, CultureInfo.InvariantCulture) : tags.Track; 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; tags.Disc = tags.Disc == 0U ? Convert.ToUInt32(mediaInfo.ParentIndexNumber, CultureInfo.InvariantCulture) : tags.Disc;
@@ -119,7 +119,8 @@ namespace MediaBrowser.Providers.TV
virtualSeason, virtualSeason,
new DeleteOptions new DeleteOptions
{ {
DeleteFileLocation = true // Internal metadata paths are removed regardless of this.
DeleteFileLocation = false
}, },
false); false);
} }
@@ -176,7 +177,8 @@ namespace MediaBrowser.Providers.TV
episode, episode,
new DeleteOptions new DeleteOptions
{ {
DeleteFileLocation = true // Internal metadata paths are removed regardless of this.
DeleteFileLocation = false
}, },
false); false);
} }
@@ -825,7 +825,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
private string GetOutputTrailerUrl(string url) private string GetOutputTrailerUrl(string url)
{ {
// This is what xbmc expects // 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) private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager)
@@ -45,27 +45,24 @@ namespace MediaBrowser.XbmcMetadata.Savers
internal static IEnumerable<string> GetMovieSavePaths(ItemInfo item) internal static IEnumerable<string> GetMovieSavePaths(ItemInfo item)
{ {
var path = item.ContainingFolderPath;
if (item.VideoType == VideoType.Dvd && !item.IsPlaceHolder) if (item.VideoType == VideoType.Dvd && !item.IsPlaceHolder)
{ {
var path = item.ContainingFolderPath;
yield return Path.Combine(path, "VIDEO_TS", "VIDEO_TS.nfo"); 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)) if (!item.IsPlaceHolder && (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay))
{ {
var path = item.ContainingFolderPath;
yield return Path.Combine(path, Path.GetFileName(path) + ".nfo"); yield return Path.Combine(path, Path.GetFileName(path) + ".nfo");
} }
else 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"); yield return Path.ChangeExtension(item.Path, ".nfo");
} }
} }
+2 -2
View File
@@ -1,4 +1,4 @@
using System.Reflection; using System.Reflection;
[assembly: AssemblyVersion("10.9.4")] [assembly: AssemblyVersion("10.9.6")]
[assembly: AssemblyFileVersion("10.9.4")] [assembly: AssemblyFileVersion("10.9.6")]
@@ -15,7 +15,7 @@
<PropertyGroup> <PropertyGroup>
<Authors>Jellyfin Contributors</Authors> <Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Extensions</PackageId> <PackageId>Jellyfin.Extensions</PackageId>
<VersionPrefix>10.9.4</VersionPrefix> <VersionPrefix>10.9.6</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
@@ -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 // Has to exist for querying DateModified time on file, results stored but not checked so not populating
BaseItem.FileSystem ??= Mock.Of<IFileSystem>(); 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}"; var path = validPaths ? _testDataImagePath.Format : "invalid path {0}";
for (int i = 0; i < count; i++) 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), FullName = string.Format(CultureInfo.InvariantCulture, path, i),
}); });
} }
return item; return item.Object;
} }
private static ILocalImageProvider GetImageProvider(ImageType type, int count, bool validPaths) private static ILocalImageProvider GetImageProvider(ImageType type, int count, bool validPaths)
@@ -47,6 +47,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Location
var movie = new Movie() { Path = "/media/movies/Avengers Endgame", VideoType = VideoType.Dvd }; var movie = new Movie() { Path = "/media/movies/Avengers Endgame", VideoType = VideoType.Dvd };
var path1 = "/media/movies/Avengers Endgame/Avengers Endgame.nfo"; var path1 = "/media/movies/Avengers Endgame/Avengers Endgame.nfo";
var path2 = "/media/movies/Avengers Endgame/VIDEO_TS/VIDEO_TS.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 // uses ContainingFolderPath which uses Operating system specific paths
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
@@ -54,12 +55,14 @@ namespace Jellyfin.XbmcMetadata.Tests.Location
movie.Path = movie.Path.Replace('/', '\\'); movie.Path = movie.Path.Replace('/', '\\');
path1 = path1.Replace('/', '\\'); path1 = path1.Replace('/', '\\');
path2 = path2.Replace('/', '\\'); path2 = path2.Replace('/', '\\');
path3 = path3.Replace('/', '\\');
} }
var paths = MovieNfoSaver.GetMovieSavePaths(new ItemInfo(movie)).ToArray(); var paths = MovieNfoSaver.GetMovieSavePaths(new ItemInfo(movie)).ToArray();
Assert.Equal(2, paths.Length); Assert.Equal(3, paths.Length);
Assert.Contains(path1, paths); Assert.Contains(path1, paths);
Assert.Contains(path2, paths); Assert.Contains(path2, paths);
Assert.Contains(path3, paths);
} }
} }
} }