mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-16 16:18:06 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b25d6d1e48 | ||
|
|
cf59140276 | ||
|
|
cc4563a477 | ||
|
|
0d984b5162 | ||
|
|
279cba008b | ||
|
|
2b5d458456 | ||
|
|
f41efb3b2c | ||
|
|
0155293c64 | ||
|
|
b78efd6b1e | ||
|
|
bfcc09db8a | ||
|
|
a46c17e19f | ||
|
|
b0bb22b650 | ||
|
|
0c039145e5 | ||
|
|
2a3c904a9f | ||
|
|
7cbdb6708b | ||
|
|
7058db2b04 | ||
|
|
8f7df590cd | ||
|
|
0f67a5ba2f | ||
|
|
19fb00b5b7 | ||
|
|
8683253c6d | ||
|
|
918a36d564 | ||
|
|
57ae0b5796 | ||
|
|
262f7dd98f | ||
|
|
4714b3af67 | ||
|
|
0359035000 | ||
|
|
b14edb8876 | ||
|
|
47c5e0c2c7 | ||
|
|
8709d94783 | ||
|
|
23b1251393 | ||
|
|
484aea1cdb | ||
|
|
d1c00ba4ed | ||
|
|
b1a5fe2f55 | ||
|
|
d7ff6d023c | ||
|
|
253e95dcba | ||
|
|
c7ce1aa4c7 | ||
|
|
3d87885577 | ||
|
|
a7e2271845 | ||
|
|
2a02abee46 |
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
62
Emby.Server.Implementations/Data/ManagedConnection.cs
Normal file
62
Emby.Server.Implementations/Data/ManagedConnection.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"))
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user