Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 478d8b07bf | |||
| c9b6ebd94f | |||
| 30fc089dd5 | |||
| 25f02658f0 | |||
| 2266a00337 | |||
| afeff31dca | |||
| 476dc01f4d | |||
| b81b674ae1 | |||
| 15eb7a25b9 | |||
| aadd57bc48 | |||
| cbbe5db813 | |||
| 4601097d3e | |||
| 10cd9a7f79 | |||
| 6cf98d4930 | |||
| 34a65980e3 | |||
| 6010bc01c3 | |||
| a00f9e1a10 | |||
| 85078d8f10 | |||
| 1606b6c0f6 | |||
| f097aad01e | |||
| bf53f1ae38 | |||
| 31237f778a | |||
| 55d245a77b | |||
| 9f35f56eaf | |||
| f2a5ccf102 | |||
| 2b78980747 | |||
| a89678074e | |||
| d813f83b4a | |||
| 37b7e953f7 | |||
| 08b64c5502 | |||
| 23a660e917 | |||
| 78eb9b2f78 | |||
| d90f504ca7 | |||
| 56104d3042 | |||
| fcec1fcc4d | |||
| 1a14902da8 | |||
| 34bdf8bf78 | |||
| 7ff3f6af6c | |||
| bd8b0c4c03 | |||
| 0c560a313a | |||
| 8882bb495c | |||
| e4078f984a | |||
| 8e5a2f565c | |||
| 8b442a7749 | |||
| b63f7a2bc0 | |||
| f9e7d5229e | |||
| b24d05bff7 | |||
| fd009fc71b | |||
| 302eea1cb7 | |||
| b116a2742e | |||
| 99a04e23d9 | |||
| 19a89d5a60 | |||
| feb20c131a | |||
| ec82023265 | |||
| e4f3f0b3b6 | |||
| 28274d4c75 | |||
| b6595e4efc | |||
| 4046ef1c13 | |||
| 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>
|
<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.7</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>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ namespace Emby.Naming.ExternalFiles
|
|||||||
pathInfo.Language = culture.ThreeLetterISOLanguageName;
|
pathInfo.Language = culture.ThreeLetterISOLanguageName;
|
||||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
|
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
pathInfo.IsHearingImpaired = true;
|
pathInfo.IsHearingImpaired = true;
|
||||||
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|||||||
@@ -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"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
var info = new FileInfo(path);
|
var info = new FileInfo(path);
|
||||||
|
|
||||||
if (info.Exists &&
|
if (info.Exists &&
|
||||||
((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden)
|
(info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden != isHidden)
|
||||||
{
|
{
|
||||||
if (isHidden)
|
if (isHidden)
|
||||||
{
|
{
|
||||||
@@ -417,8 +417,8 @@ namespace Emby.Server.Implementations.IO
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
|
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly == readOnly
|
||||||
&& ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
|
&& (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden == isHidden)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
@@ -1884,7 +1884,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var index = item.GetImageIndex(img);
|
var index = item.GetImageIndex(img);
|
||||||
image = await ConvertImageToLocal(item, img, index, removeOnFailure: true).ConfigureAwait(false);
|
image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
Exempt,0
|
Exempt,0
|
||||||
G,0
|
G,0
|
||||||
7+,7
|
7+,7
|
||||||
|
PG,15
|
||||||
M,15
|
M,15
|
||||||
MA,15
|
MA,15
|
||||||
MA15+,15
|
MA15+,15
|
||||||
MA 15+,15
|
MA 15+,15
|
||||||
PG,16
|
|
||||||
16+,16
|
16+,16
|
||||||
R,18
|
R,18
|
||||||
R18+,18
|
R18+,18
|
||||||
|
|||||||
|
@@ -170,8 +170,13 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
private List<Playlist> GetUserPlaylists(Guid userId)
|
private List<Playlist> GetUserPlaylists(Guid userId)
|
||||||
{
|
{
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
|
var playlistsFolder = GetPlaylistsFolder(userId);
|
||||||
|
if (playlistsFolder is null)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return GetPlaylistsFolder(userId).GetChildren(user, true).OfType<Playlist>().ToList();
|
return playlistsFolder.GetChildren(user, true).OfType<Playlist>().ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetTargetPath(string path)
|
private static string GetTargetPath(string path)
|
||||||
@@ -184,11 +189,11 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, MediaType playlistMediaType, User user, DtoOptions options)
|
private IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<Guid> itemIds, User user, DtoOptions options)
|
||||||
{
|
{
|
||||||
var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null);
|
var items = itemIds.Select(_libraryManager.GetItemById).Where(i => i is not null);
|
||||||
|
|
||||||
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
|
return Playlist.GetPlaylistItems(items, user, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
|
public Task AddItemToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
|
||||||
@@ -208,7 +213,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||||||
?? throw new ArgumentException("No Playlist exists with Id " + playlistId);
|
?? throw new ArgumentException("No Playlist exists with Id " + playlistId);
|
||||||
|
|
||||||
// Retrieve all the items to be added to the playlist
|
// Retrieve all the items to be added to the playlist
|
||||||
var newItems = GetPlaylistItems(newItemIds, playlist.MediaType, user, options)
|
var newItems = GetPlaylistItems(newItemIds, user, options)
|
||||||
.Where(i => i.SupportsAddingToPlaylist);
|
.Where(i => i.SupportsAddingToPlaylist);
|
||||||
|
|
||||||
// Filter out duplicate items, if necessary
|
// Filter out duplicate items, if necessary
|
||||||
|
|||||||
@@ -106,13 +106,20 @@ public partial class AudioNormalizationTask : IScheduledTask
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tempFile = Path.Join(_configurationManager.GetTranscodePath(), Guid.NewGuid() + ".concat");
|
_logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
|
||||||
|
var tempFile = Path.Join(_configurationManager.GetTranscodePath(), a.Id + ".concat");
|
||||||
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
|
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
|
||||||
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
|
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
|
||||||
a.LUFS = await CalculateLUFSAsync(
|
try
|
||||||
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
{
|
||||||
cancellationToken).ConfigureAwait(false);
|
a.LUFS = await CalculateLUFSAsync(
|
||||||
File.Delete(tempFile);
|
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
File.Delete(tempFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_itemRepository.SaveItems(albums, cancellationToken);
|
_itemRepository.SaveItems(albums, cancellationToken);
|
||||||
|
|||||||
+1
-8
@@ -127,15 +127,8 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
|
|||||||
{
|
{
|
||||||
_logger.LogDebug("Updating {FolderName}", folder.Name);
|
_logger.LogDebug("Updating {FolderName}", folder.Name);
|
||||||
folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
|
folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
|
||||||
|
_providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit);
|
||||||
folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
|
folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
|
||||||
|
|
||||||
_providerManager.QueueRefresh(
|
|
||||||
folder.Id,
|
|
||||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
|
||||||
{
|
|
||||||
ForceSave = true
|
|
||||||
},
|
|
||||||
RefreshPriority.High);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
@@ -133,53 +134,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
DeleteFile(file.FullName);
|
FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteEmptyFolders(directory);
|
FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeleteEmptyFolders(string parent)
|
|
||||||
{
|
|
||||||
foreach (var directory in _fileSystem.GetDirectoryPaths(parent))
|
|
||||||
{
|
|
||||||
DeleteEmptyFolders(directory);
|
|
||||||
if (!_fileSystem.GetFileSystemEntryPaths(directory).Any())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(directory, false);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting directory {Path}", directory);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting directory {Path}", directory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeleteFile(string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_fileSystem.DeleteFile(path);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting file {Path}", path);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting file {Path}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
@@ -113,53 +113,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
DeleteFile(file.FullName);
|
FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteEmptyFolders(directory);
|
FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
|
||||||
|
|
||||||
progress.Report(100);
|
progress.Report(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeleteEmptyFolders(string parent)
|
|
||||||
{
|
|
||||||
foreach (var directory in _fileSystem.GetDirectoryPaths(parent))
|
|
||||||
{
|
|
||||||
DeleteEmptyFolders(directory);
|
|
||||||
if (!_fileSystem.GetFileSystemEntryPaths(directory).Any())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(directory, false);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting directory {Path}", directory);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting directory {Path}", directory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeleteFile(string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_fileSystem.DeleteFile(path);
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting file {Path}", path);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error deleting file {Path}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ public class ItemRefreshController : BaseJellyfinApiController
|
|||||||
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|
||||||
|| replaceAllImages
|
|| replaceAllImages
|
||||||
|| replaceAllMetadata,
|
|| replaceAllMetadata,
|
||||||
IsAutomated = false
|
IsAutomated = false,
|
||||||
|
RemoveOldMetadata = replaceAllMetadata
|
||||||
};
|
};
|
||||||
|
|
||||||
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -154,6 +154,11 @@ public static class StreamingHelpers
|
|||||||
// Some channels from HDHomerun will experience A/V sync issues
|
// Some channels from HDHomerun will experience A/V sync issues
|
||||||
streamingRequest.SegmentContainer = "ts";
|
streamingRequest.SegmentContainer = "ts";
|
||||||
streamingRequest.VideoCodec = "h264";
|
streamingRequest.VideoCodec = "h264";
|
||||||
|
streamingRequest.AudioCodec = "aac";
|
||||||
|
state.SupportedVideoCodecs = ["h264"];
|
||||||
|
state.Request.VideoCodec = "h264";
|
||||||
|
state.SupportedAudioCodecs = ["aac"];
|
||||||
|
state.Request.AudioCodec = "aac";
|
||||||
}
|
}
|
||||||
|
|
||||||
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
|
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -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.7</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.7</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>
|
||||||
|
|||||||
@@ -1949,14 +1949,15 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove it from the item
|
// Remove from file system
|
||||||
RemoveImage(info);
|
|
||||||
|
|
||||||
if (info.IsLocalFile)
|
if (info.IsLocalFile)
|
||||||
{
|
{
|
||||||
FileSystem.DeleteFile(info.Path);
|
FileSystem.DeleteFile(info.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove from item
|
||||||
|
RemoveImage(info);
|
||||||
|
|
||||||
await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
await UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Security;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -364,15 +365,23 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
if (IsFileProtocol)
|
if (IsFileProtocol)
|
||||||
{
|
{
|
||||||
IEnumerable<BaseItem> nonCachedChildren;
|
IEnumerable<BaseItem> nonCachedChildren = [];
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
nonCachedChildren = GetNonCachedChildren(directoryService);
|
nonCachedChildren = GetNonCachedChildren(directoryService);
|
||||||
}
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error retrieving children from file system");
|
||||||
|
}
|
||||||
|
catch (SecurityException ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error retrieving children from file system");
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error retrieving children folder");
|
Logger.LogError(ex, "Error retrieving children");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -350,17 +350,10 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
|
|
||||||
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||||
{
|
{
|
||||||
var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
|
|
||||||
|
|
||||||
// add optimization when this setting is not enabled
|
|
||||||
var seriesKey = queryFromSeries ?
|
|
||||||
GetUniqueSeriesKey(this) :
|
|
||||||
GetUniqueSeriesKey(parentSeason);
|
|
||||||
|
|
||||||
var query = new InternalItemsQuery(user)
|
var query = new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
|
AncestorWithPresentationUniqueKey = null,
|
||||||
SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
|
SeriesPresentationUniqueKey = GetUniqueSeriesKey(this),
|
||||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||||
DtoOptions = options
|
DtoOptions = options
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using MediaBrowser.Model.IO;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller.IO;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper methods for file system management.
|
||||||
|
/// </summary>
|
||||||
|
public static class FileSystemHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileSystem">The fileSystem.</param>
|
||||||
|
/// <param name="path">The path.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
public static void DeleteFile(IFileSystem fileSystem, string path, ILogger logger)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fileSystem.DeleteFile(path);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error deleting file {Path}", path);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error deleting file {Path}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recursively delete empty folders.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileSystem">The fileSystem.</param>
|
||||||
|
/// <param name="path">The path.</param>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
public static void DeleteEmptyFolders(IFileSystem fileSystem, string path, ILogger logger)
|
||||||
|
{
|
||||||
|
foreach (var directory in fileSystem.GetDirectoryPaths(path))
|
||||||
|
{
|
||||||
|
DeleteEmptyFolders(fileSystem, directory, logger);
|
||||||
|
if (!fileSystem.GetFileSystemEntryPaths(directory).Any())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(directory, false);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error deleting directory {Path}", directory);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error deleting directory {Path}", directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.7</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>
|
||||||
|
|||||||
@@ -1208,8 +1208,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
var subtitlePath = state.SubtitleStream.Path;
|
var subtitlePath = state.SubtitleStream.Path;
|
||||||
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
|
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
|
||||||
|
|
||||||
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
|
// dvdsub/vobsub graphical subtitles use .sub+.idx pairs
|
||||||
|| subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase))
|
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
|
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
|
||||||
if (File.Exists(idxFile))
|
if (File.Exists(idxFile))
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ namespace MediaBrowser.Controller.Playlists
|
|||||||
return base.GetChildren(user, true, query);
|
return base.GetChildren(user, true, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IReadOnlyList<BaseItem> GetPlaylistItems(MediaType playlistMediaType, IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
|
public static IReadOnlyList<BaseItem> GetPlaylistItems(IEnumerable<BaseItem> inputItems, User user, DtoOptions options)
|
||||||
{
|
{
|
||||||
if (user is not null)
|
if (user is not null)
|
||||||
{
|
{
|
||||||
@@ -177,14 +177,14 @@ namespace MediaBrowser.Controller.Playlists
|
|||||||
|
|
||||||
foreach (var item in inputItems)
|
foreach (var item in inputItems)
|
||||||
{
|
{
|
||||||
var playlistItems = GetPlaylistItems(item, user, playlistMediaType, options);
|
var playlistItems = GetPlaylistItems(item, user, options);
|
||||||
list.AddRange(playlistItems);
|
list.AddRange(playlistItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, MediaType mediaType, DtoOptions options)
|
private static IEnumerable<BaseItem> GetPlaylistItems(BaseItem item, User user, DtoOptions options)
|
||||||
{
|
{
|
||||||
if (item is MusicGenre musicGenre)
|
if (item is MusicGenre musicGenre)
|
||||||
{
|
{
|
||||||
@@ -216,7 +216,7 @@ namespace MediaBrowser.Controller.Playlists
|
|||||||
{
|
{
|
||||||
Recursive = true,
|
Recursive = true,
|
||||||
IsFolder = false,
|
IsFolder = false,
|
||||||
MediaTypes = [mediaType],
|
MediaTypes = [MediaType.Audio, MediaType.Video],
|
||||||
EnableTotalRecordCount = false,
|
EnableTotalRecordCount = false,
|
||||||
DtoOptions = options
|
DtoOptions = options
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,26 @@ 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 images = GetImageFilesFromFolder(nameWithoutExtension, parentPathFiles);
|
||||||
|
|
||||||
return GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles);
|
var metadataSubDir = directoryService.GetDirectories(parentPath).FirstOrDefault(d => d.Name.Equals("metadata", StringComparison.Ordinal));
|
||||||
|
if (metadataSubDir is not null)
|
||||||
|
{
|
||||||
|
var files = directoryService.GetFiles(metadataSubDir.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 list = new List<LocalImageInfo>(1);
|
||||||
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
|
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
|
||||||
|
|
||||||
var list = new List<LocalImageInfo>(1);
|
foreach (var i in filePaths)
|
||||||
|
|
||||||
foreach (var i in parentPathFiles)
|
|
||||||
{
|
{
|
||||||
if (i.IsDirectory)
|
if (i.IsDirectory)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1155,10 +1155,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
// Get all files from the BDMV/STREAMING directory
|
// Get all files from the BDMV/STREAMING directory
|
||||||
// Only return playable local .m2ts files
|
// Only return playable local .m2ts files
|
||||||
|
var files = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM")).ToList();
|
||||||
return validPlaybackFiles
|
return validPlaybackFiles
|
||||||
.Select(f => _fileSystem.GetFileInfo(Path.Join(path, "BDMV", "STREAM", f)))
|
.Select(validFile => files.FirstOrDefault(f => Path.GetFileName(f.FullName.AsSpan()).Equals(validFile, StringComparison.OrdinalIgnoreCase))?.FullName)
|
||||||
.Where(f => f.Exists)
|
.Where(f => f is not null)
|
||||||
.Select(f => f.FullName)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -280,8 +280,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
splitFormat[i] = "mpeg";
|
splitFormat[i] = "mpeg";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle MPEG-2 container
|
// Handle MPEG-TS container
|
||||||
else if (string.Equals(splitFormat[i], "mpeg", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(splitFormat[i], "mpegts", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
splitFormat[i] = "ts";
|
splitFormat[i] = "ts";
|
||||||
}
|
}
|
||||||
@@ -624,15 +624,19 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
{
|
{
|
||||||
if (string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
codec = "dvbsub";
|
codec = "DVBSUB";
|
||||||
}
|
}
|
||||||
else if ((codec ?? string.Empty).Contains("PGS", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(codec, "dvb_teletext", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
codec = "PGSSUB";
|
codec = "DVBTXT";
|
||||||
}
|
}
|
||||||
else if ((codec ?? string.Empty).Contains("DVD", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(codec, "dvd_subtitle", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
codec = "DVDSUB";
|
codec = "DVDSUB"; // .sub+.idx
|
||||||
|
}
|
||||||
|
else if (string.Equals(codec, "hdmv_pgs_subtitle", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
codec = "PGSSUB"; // .sup
|
||||||
}
|
}
|
||||||
|
|
||||||
return codec;
|
return codec;
|
||||||
@@ -779,11 +783,10 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
|
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (isAudio
|
if (isAudio
|
||||||
&& (string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase))
|
||||||
|| string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
{
|
||||||
stream.Type = MediaStreamType.EmbeddedImage;
|
stream.Type = MediaStreamType.EmbeddedImage;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -656,14 +656,14 @@ namespace MediaBrowser.Model.Entities
|
|||||||
{
|
{
|
||||||
string codec = format ?? string.Empty;
|
string codec = format ?? string.Empty;
|
||||||
|
|
||||||
// sub = external .sub file
|
// microdvd and dvdsub/vobsub share the ".sub" file extension, but it's text-based.
|
||||||
|
|
||||||
return !codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)
|
return codec.Contains("microdvd", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !codec.Contains("dvd", StringComparison.OrdinalIgnoreCase)
|
|| (!codec.Contains("pgs", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
|
&& !codec.Contains("dvdsub", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase)
|
&& !codec.Contains("dvbsub", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase)
|
&& !string.Equals(codec, "sup", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !string.Equals(codec, "dvb_subtitle", StringComparison.OrdinalIgnoreCase);
|
&& !string.Equals(codec, "sub", StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool SupportsSubtitleConversionTo(string toCodec)
|
public bool SupportsSubtitleConversionTo(string toCodec)
|
||||||
|
|||||||
@@ -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.7</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>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using MediaBrowser.Common.Configuration;
|
|||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Controller.IO;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
@@ -100,8 +101,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 +127,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 +188,29 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_fileSystem.DeleteFile(currentPath);
|
_fileSystem.DeleteFile(currentPath);
|
||||||
|
|
||||||
|
// Remove local episode metadata directory if it exists and is empty
|
||||||
|
var directory = Path.GetDirectoryName(currentPath);
|
||||||
|
if (item is Episode && directory.Equals("metadata", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var parentDirectoryPath = Directory.GetParent(currentPath).FullName;
|
||||||
|
if (_fileSystem.DirectoryExists(parentDirectoryPath) && !_fileSystem.GetFiles(parentDirectoryPath).Any())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Deleting empty local metadata folder {Folder}", parentDirectoryPath);
|
||||||
|
Directory.Delete(parentDirectoryPath);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting directory {Path}", parentDirectoryPath);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting directory {Path}", parentDirectoryPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
@@ -374,6 +402,47 @@ 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
|
||||||
|
&& (imageIndex is null || imageIndex == 0))
|
||||||
|
{
|
||||||
|
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 +516,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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Controller.Providers;
|
using MediaBrowser.Controller.Providers;
|
||||||
@@ -96,7 +97,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions)
|
public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions)
|
||||||
{
|
{
|
||||||
var hasChanges = false;
|
var hasChanges = false;
|
||||||
IDirectoryService directoryService = refreshOptions?.DirectoryService;
|
var directoryService = refreshOptions?.DirectoryService;
|
||||||
|
|
||||||
if (item is not Photo)
|
if (item is not Photo)
|
||||||
{
|
{
|
||||||
@@ -158,7 +159,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// only delete existing multi-images if new ones were added
|
// Only delete existing multi-images if new ones were added
|
||||||
if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count())
|
if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count())
|
||||||
{
|
{
|
||||||
PruneImages(item, oldBackdropImages);
|
PruneImages(item, oldBackdropImages);
|
||||||
@@ -359,10 +360,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
private void PruneImages(BaseItem item, IReadOnlyList<ItemImageInfo> images)
|
private void PruneImages(BaseItem item, IReadOnlyList<ItemImageInfo> images)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < images.Count; i++)
|
foreach (var image in images)
|
||||||
{
|
{
|
||||||
var image = images[i];
|
|
||||||
|
|
||||||
if (image.IsLocalFile)
|
if (image.IsLocalFile)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -371,7 +370,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
// nothing to do, already gone
|
// Nothing to do, already gone
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
@@ -381,6 +380,16 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
|
|
||||||
item.RemoveImages(images);
|
item.RemoveImages(images);
|
||||||
|
|
||||||
|
// Cleanup old metadata directory for episodes if empty
|
||||||
|
if (item is Episode)
|
||||||
|
{
|
||||||
|
var oldLocalMetadataDirectory = Path.Combine(item.ContainingFolderPath, "metadata");
|
||||||
|
if (_fileSystem.DirectoryExists(oldLocalMetadataDirectory) && !_fileSystem.GetFiles(oldLocalMetadataDirectory).Any())
|
||||||
|
{
|
||||||
|
Directory.Delete(oldLocalMetadataDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -413,12 +422,10 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
var changed = item.ValidateImages();
|
var changed = item.ValidateImages();
|
||||||
var foundImageTypes = new List<ImageType>();
|
var foundImageTypes = new List<ImageType>();
|
||||||
|
|
||||||
for (var i = 0; i < _singularImages.Length; i++)
|
for (var i = 0; i < _singularImages.Length; i++)
|
||||||
{
|
{
|
||||||
var type = _singularImages[i];
|
var type = _singularImages[i];
|
||||||
var image = GetFirstLocalImageInfoByType(images, type);
|
var image = GetFirstLocalImageInfoByType(images, type);
|
||||||
|
|
||||||
if (image is not null)
|
if (image is not null)
|
||||||
{
|
{
|
||||||
var currentImage = item.GetImageInfo(type, 0);
|
var currentImage = item.GetImageInfo(type, 0);
|
||||||
|
|||||||
@@ -92,10 +92,6 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var localImagesFailed = false;
|
|
||||||
|
|
||||||
var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
|
|
||||||
|
|
||||||
if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
|
if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
|
||||||
{
|
{
|
||||||
if (ImageProvider.RemoveImages(item))
|
if (ImageProvider.RemoveImages(item))
|
||||||
@@ -104,19 +100,29 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start by validating images
|
var localImagesFailed = false;
|
||||||
try
|
var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
|
||||||
|
|
||||||
|
// Only validate already registered images if we are replacing and saving locally
|
||||||
|
if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages)
|
||||||
{
|
{
|
||||||
// Always validate images and check for new locally stored ones.
|
item.ValidateImages();
|
||||||
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
|
|
||||||
{
|
|
||||||
updateType |= ItemUpdateType.ImageUpdate;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
else
|
||||||
{
|
{
|
||||||
localImagesFailed = true;
|
// Run full image validation and register new local images
|
||||||
Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
|
try
|
||||||
|
{
|
||||||
|
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
|
||||||
|
{
|
||||||
|
updateType |= ItemUpdateType.ImageUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
localImagesFailed = true;
|
||||||
|
Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var metadataResult = new MetadataResult<TItemType>
|
var metadataResult = new MetadataResult<TItemType>
|
||||||
@@ -154,7 +160,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 +646,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
|
||||||
@@ -667,71 +675,78 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
};
|
};
|
||||||
temp.Item.Path = item.Path;
|
temp.Item.Path = item.Path;
|
||||||
temp.Item.Id = item.Id;
|
temp.Item.Id = item.Id;
|
||||||
|
temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
|
||||||
|
temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
|
||||||
|
|
||||||
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 +778,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -804,19 +819,16 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
var refreshResult = new RefreshResult();
|
var refreshResult = new RefreshResult();
|
||||||
|
|
||||||
var tmpDataMerged = false;
|
if (id is not null)
|
||||||
|
{
|
||||||
|
MergeNewData(temp.Item, id);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var provider in providers)
|
foreach (var provider in providers)
|
||||||
{
|
{
|
||||||
var providerName = provider.GetType().Name;
|
var providerName = provider.GetType().Name;
|
||||||
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
||||||
|
|
||||||
if (id is not null && !tmpDataMerged)
|
|
||||||
{
|
|
||||||
MergeNewData(temp.Item, id);
|
|
||||||
tmpDataMerged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
|
var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -1050,7 +1062,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 +1092,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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
using MediaBrowser.Controller.Entities.Audio;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -136,6 +137,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
if (!audio.IsLocked)
|
if (!audio.IsLocked)
|
||||||
{
|
{
|
||||||
await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false);
|
await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false);
|
||||||
|
if (tryExtractEmbeddedLyrics)
|
||||||
|
{
|
||||||
|
AddExternalLyrics(audio, mediaStreams, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
|
audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
|
||||||
@@ -193,11 +198,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;
|
||||||
@@ -369,7 +374,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, false);
|
var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, false);
|
||||||
|
|
||||||
audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
|
audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
|
||||||
currentStreams.AddRange(externalLyricFiles);
|
if (externalLyricFiles.Count > 0)
|
||||||
|
{
|
||||||
|
currentStreams.Add(externalLyricFiles[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,6 +358,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate;
|
blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate;
|
||||||
blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width;
|
blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width;
|
||||||
blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height;
|
blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height;
|
||||||
|
blurayVideoStream.ColorRange = ffmpegVideoStream.ColorRange;
|
||||||
|
blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace;
|
||||||
|
blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer;
|
||||||
|
blurayVideoStream.ColorPrimaries = ffmpegVideoStream.ColorPrimaries;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -18,182 +16,212 @@ using MediaBrowser.Model.IO;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PlaylistsNET.Content;
|
using PlaylistsNET.Content;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.Playlists
|
namespace MediaBrowser.Providers.Playlists;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Local playlist provider.
|
||||||
|
/// </summary>
|
||||||
|
public class PlaylistItemsProvider : ILocalMetadataProvider<Playlist>,
|
||||||
|
IHasOrder,
|
||||||
|
IForcedProvider,
|
||||||
|
IHasItemChangeMonitor
|
||||||
{
|
{
|
||||||
public class PlaylistItemsProvider : ICustomMetadataProvider<Playlist>,
|
private readonly IFileSystem _fileSystem;
|
||||||
IHasOrder,
|
private readonly ILibraryManager _libraryManager;
|
||||||
IForcedProvider,
|
private readonly ILogger<PlaylistItemsProvider> _logger;
|
||||||
IPreRefreshProvider,
|
private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists];
|
||||||
IHasItemChangeMonitor
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="PlaylistItemsProvider"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{PlaylistItemsProvider}"/> interface.</param>
|
||||||
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
|
public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger, ILibraryManager libraryManager, IFileSystem fileSystem)
|
||||||
{
|
{
|
||||||
private readonly IFileSystem _fileSystem;
|
_logger = logger;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly ILogger<PlaylistItemsProvider> _logger;
|
_fileSystem = fileSystem;
|
||||||
private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists];
|
}
|
||||||
|
|
||||||
public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger, ILibraryManager libraryManager, IFileSystem fileSystem)
|
/// <inheritdoc />
|
||||||
|
public string Name => "Playlist Item Provider";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int Order => 100;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<MetadataResult<Playlist>> GetMetadata(
|
||||||
|
ItemInfo info,
|
||||||
|
IDirectoryService directoryService,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = new MetadataResult<Playlist>()
|
||||||
{
|
{
|
||||||
_logger = logger;
|
Item = new Playlist
|
||||||
_libraryManager = libraryManager;
|
{
|
||||||
_fileSystem = fileSystem;
|
Path = info.Path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Fetch(result);
|
||||||
|
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Fetch(MetadataResult<Playlist> result)
|
||||||
|
{
|
||||||
|
var item = result.Item;
|
||||||
|
var path = item.Path;
|
||||||
|
if (!Playlist.IsPlaylistFile(path))
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name => "Playlist Reader";
|
var extension = Path.GetExtension(path);
|
||||||
|
if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||||
// Run last
|
|
||||||
public int Order => 100;
|
|
||||||
|
|
||||||
public Task<ItemUpdateType> FetchAsync(Playlist item, MetadataRefreshOptions options, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
var path = item.Path;
|
return;
|
||||||
if (!Playlist.IsPlaylistFile(path))
|
}
|
||||||
{
|
|
||||||
return Task.FromResult(ItemUpdateType.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(path);
|
|
||||||
if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return Task.FromResult(ItemUpdateType.None);
|
|
||||||
}
|
|
||||||
|
|
||||||
var items = GetItems(path, extension).ToArray();
|
|
||||||
|
|
||||||
|
var items = GetItems(path, extension).ToArray();
|
||||||
|
if (items.Length > 0)
|
||||||
|
{
|
||||||
|
result.HasMetadata = true;
|
||||||
item.LinkedChildren = items;
|
item.LinkedChildren = items;
|
||||||
|
|
||||||
return Task.FromResult(ItemUpdateType.MetadataImport);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetItems(string path, string extension)
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<LinkedChild> GetItems(string path, string extension)
|
||||||
|
{
|
||||||
|
var libraryRoots = _libraryManager.GetUserRootFolder().Children
|
||||||
|
.OfType<CollectionFolder>()
|
||||||
|
.Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Value))
|
||||||
|
.SelectMany(f => f.PhysicalLocations)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
using (var stream = File.OpenRead(path))
|
||||||
{
|
{
|
||||||
var libraryRoots = _libraryManager.GetUserRootFolder().Children
|
if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
.OfType<CollectionFolder>()
|
|
||||||
.Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Value))
|
|
||||||
.SelectMany(f => f.PhysicalLocations)
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
using (var stream = File.OpenRead(path))
|
|
||||||
{
|
{
|
||||||
if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
|
return GetWplItems(stream, path, libraryRoots);
|
||||||
{
|
|
||||||
return GetWplItems(stream, path, libraryRoots);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetZplItems(stream, path, libraryRoots);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetM3uItems(stream, path, libraryRoots);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetM3uItems(stream, path, libraryRoots);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return GetPlsItems(stream, path, libraryRoots);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Enumerable.Empty<LinkedChild>();
|
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
|
||||||
{
|
|
||||||
var content = new PlsContent();
|
|
||||||
var playlist = content.GetFromStream(stream);
|
|
||||||
|
|
||||||
return playlist.PlaylistEntries
|
|
||||||
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
|
||||||
.Where(i => i is not null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetM3uItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
|
||||||
{
|
|
||||||
var content = new M3uContent();
|
|
||||||
var playlist = content.GetFromStream(stream);
|
|
||||||
|
|
||||||
return playlist.PlaylistEntries
|
|
||||||
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
|
||||||
.Where(i => i is not null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetZplItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
|
||||||
{
|
|
||||||
var content = new ZplContent();
|
|
||||||
var playlist = content.GetFromStream(stream);
|
|
||||||
|
|
||||||
return playlist.PlaylistEntries
|
|
||||||
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
|
||||||
.Where(i => i is not null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<LinkedChild> GetWplItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
|
||||||
{
|
|
||||||
var content = new WplContent();
|
|
||||||
var playlist = content.GetFromStream(stream);
|
|
||||||
|
|
||||||
return playlist.PlaylistEntries
|
|
||||||
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
|
||||||
.Where(i => i is not null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private LinkedChild GetLinkedChild(string itemPath, string playlistPath, List<string> libraryRoots)
|
|
||||||
{
|
|
||||||
if (TryGetPlaylistItemPath(itemPath, playlistPath, libraryRoots, out var parsedPath))
|
|
||||||
{
|
{
|
||||||
return new LinkedChild
|
return GetZplItems(stream, path, libraryRoots);
|
||||||
{
|
|
||||||
Path = parsedPath,
|
|
||||||
Type = LinkedChildType.Manual
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return GetM3uItems(stream, path, libraryRoots);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return GetM3uItems(stream, path, libraryRoots);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return GetPlsItems(stream, path, libraryRoots);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryGetPlaylistItemPath(string itemPath, string playlistPath, List<string> libraryPaths, out string path)
|
return Enumerable.Empty<LinkedChild>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
|
{
|
||||||
|
var content = new PlsContent();
|
||||||
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
|
return playlist.PlaylistEntries
|
||||||
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
|
.Where(i => i is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<LinkedChild> GetM3uItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
|
{
|
||||||
|
var content = new M3uContent();
|
||||||
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
|
return playlist.PlaylistEntries
|
||||||
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
|
.Where(i => i is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<LinkedChild> GetZplItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
|
{
|
||||||
|
var content = new ZplContent();
|
||||||
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
|
return playlist.PlaylistEntries
|
||||||
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
|
.Where(i => i is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<LinkedChild> GetWplItems(Stream stream, string playlistPath, List<string> libraryRoots)
|
||||||
|
{
|
||||||
|
var content = new WplContent();
|
||||||
|
var playlist = content.GetFromStream(stream);
|
||||||
|
|
||||||
|
return playlist.PlaylistEntries
|
||||||
|
.Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
|
||||||
|
.Where(i => i is not null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinkedChild GetLinkedChild(string itemPath, string playlistPath, List<string> libraryRoots)
|
||||||
|
{
|
||||||
|
if (TryGetPlaylistItemPath(itemPath, playlistPath, libraryRoots, out var parsedPath))
|
||||||
{
|
{
|
||||||
path = null;
|
return new LinkedChild
|
||||||
string pathToCheck = _fileSystem.MakeAbsolutePath(Path.GetDirectoryName(playlistPath), itemPath);
|
|
||||||
if (!File.Exists(pathToCheck))
|
|
||||||
{
|
{
|
||||||
return false;
|
Path = parsedPath,
|
||||||
}
|
Type = LinkedChildType.Manual
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var libraryPath in libraryPaths)
|
return null;
|
||||||
{
|
}
|
||||||
if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
path = pathToCheck;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private bool TryGetPlaylistItemPath(string itemPath, string playlistPath, List<string> libraryPaths, out string path)
|
||||||
|
{
|
||||||
|
path = null;
|
||||||
|
string pathToCheck = _fileSystem.MakeAbsolutePath(Path.GetDirectoryName(playlistPath), itemPath);
|
||||||
|
if (!File.Exists(pathToCheck))
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
foreach (var libraryPath in libraryPaths)
|
||||||
{
|
{
|
||||||
var path = item.Path;
|
if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
|
|
||||||
{
|
{
|
||||||
var file = directoryService.GetFile(path);
|
path = pathToCheck;
|
||||||
if (file is not null && file.LastWriteTimeUtc != item.DateModified)
|
return true;
|
||||||
{
|
|
||||||
_logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||||
|
{
|
||||||
|
var path = item.Path;
|
||||||
|
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
|
||||||
|
{
|
||||||
|
var file = directoryService.GetFile(path);
|
||||||
|
if (file is not null && file.LastWriteTimeUtc != item.DateModified)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
|
|||||||
// If we have a release ID but not a release group ID, lookup the release group
|
// If we have a release ID but not a release group ID, lookup the release group
|
||||||
if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
|
if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId))
|
||||||
{
|
{
|
||||||
var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Releases, cancellationToken).ConfigureAwait(false);
|
var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
|
||||||
releaseGroupId = release.ReleaseGroup?.Id.ToString();
|
releaseGroupId = release.ReleaseGroup?.Id.ToString();
|
||||||
result.HasMetadata = true;
|
result.HasMetadata = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ namespace MediaBrowser.Providers.TV
|
|||||||
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
|
await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
RemoveObsoleteEpisodes(item);
|
RemoveObsoleteEpisodes(item);
|
||||||
RemoveObsoleteSeasons(item);
|
|
||||||
await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
|
||||||
|
RemoveObsoleteSeasons(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -91,7 +91,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
private void RemoveObsoleteSeasons(Series series)
|
private void RemoveObsoleteSeasons(Series series)
|
||||||
{
|
{
|
||||||
// TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in UpdateAndCreateSeasonsAsync.
|
// TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in CreateSeasonsAsync.
|
||||||
var physicalSeasonNumbers = new HashSet<int>();
|
var physicalSeasonNumbers = new HashSet<int>();
|
||||||
var virtualSeasons = new List<Season>();
|
var virtualSeasons = new List<Season>();
|
||||||
foreach (var existingSeason in series.Children.OfType<Season>())
|
foreach (var existingSeason in series.Children.OfType<Season>())
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -201,11 +203,16 @@ namespace MediaBrowser.Providers.TV
|
|||||||
foreach (var seasonNumber in uniqueSeasonNumbers)
|
foreach (var seasonNumber in uniqueSeasonNumbers)
|
||||||
{
|
{
|
||||||
// Null season numbers will have a 'dummy' season created because seasons are always required.
|
// Null season numbers will have a 'dummy' season created because seasons are always required.
|
||||||
if (!seasons.Any(i => i.IndexNumber == seasonNumber))
|
var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
|
||||||
|
if (existingSeason is null)
|
||||||
{
|
{
|
||||||
var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
|
var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
|
||||||
var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
|
await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
|
||||||
series.AddChild(season);
|
}
|
||||||
|
else if (existingSeason.IsVirtualItem)
|
||||||
|
{
|
||||||
|
existingSeason.IsVirtualItem = false;
|
||||||
|
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,7 +225,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
/// <param name="seasonNumber">The season number.</param>
|
/// <param name="seasonNumber">The season number.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>The newly created season.</returns>
|
/// <returns>The newly created season.</returns>
|
||||||
private async Task<Season> CreateSeasonAsync(
|
private async Task CreateSeasonAsync(
|
||||||
Series series,
|
Series series,
|
||||||
string? seasonName,
|
string? seasonName,
|
||||||
int? seasonNumber,
|
int? seasonNumber,
|
||||||
@@ -235,14 +242,12 @@ namespace MediaBrowser.Providers.TV
|
|||||||
typeof(Season)),
|
typeof(Season)),
|
||||||
IsVirtualItem = false,
|
IsVirtualItem = false,
|
||||||
SeriesId = series.Id,
|
SeriesId = series.Id,
|
||||||
SeriesName = series.Name
|
SeriesName = series.Name,
|
||||||
|
SeriesPresentationUniqueKey = series.GetPresentationUniqueKey()
|
||||||
};
|
};
|
||||||
|
|
||||||
series.AddChild(season);
|
series.AddChild(season);
|
||||||
|
|
||||||
await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
|
await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
return season;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
|
private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber)
|
||||||
|
|||||||
@@ -519,7 +519,9 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
|||||||
if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate))
|
if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate))
|
||||||
{
|
{
|
||||||
item.PremiereDate = releaseDate;
|
item.PremiereDate = releaseDate;
|
||||||
item.ProductionYear = releaseDate.Year;
|
|
||||||
|
// Production year can already be set by the year tag
|
||||||
|
item.ProductionYear ??= releaseDate.Year;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -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
@@ -1,4 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
[assembly: AssemblyVersion("10.9.4")]
|
[assembly: AssemblyVersion("10.9.7")]
|
||||||
[assembly: AssemblyFileVersion("10.9.4")]
|
[assembly: AssemblyFileVersion("10.9.7")]
|
||||||
|
|||||||
@@ -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.7</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>
|
||||||
|
|||||||
@@ -919,10 +919,14 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(address);
|
ArgumentNullException.ThrowIfNull(address);
|
||||||
|
|
||||||
// See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
|
// Map IPv6 mapped IPv4 back to IPv4 (happens if Kestrel runs in dual-socket mode)
|
||||||
|
if (address.IsIPv4MappedToIPv6)
|
||||||
|
{
|
||||||
|
address = address.MapToIPv4();
|
||||||
|
}
|
||||||
|
|
||||||
if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
|
if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||||
|| address.Equals(IPAddress.Loopback)
|
|| IPAddress.IsLoopback(address))
|
||||||
|| address.Equals(IPAddress.IPv6Loopback))
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ public class ExternalPathParserTests
|
|||||||
[InlineData(".en.cc.title", "title", "eng", false, false, true)]
|
[InlineData(".en.cc.title", "title", "eng", false, false, true)]
|
||||||
[InlineData(".hi.en.title", "title", "eng", false, false, true)]
|
[InlineData(".hi.en.title", "title", "eng", false, false, true)]
|
||||||
[InlineData(".en.hi.title", "title", "eng", false, false, true)]
|
[InlineData(".en.hi.title", "title", "eng", false, false, true)]
|
||||||
|
[InlineData(".Subs for Chinese Audio.eng", "Subs for Chinese Audio", "eng", false, false, false)]
|
||||||
public void ParseFile_ExtraTokens_ParseToValues(string tokens, string? title, string? language, bool isDefault = false, bool isForced = false, bool isHearingImpaired = false)
|
public void ParseFile_ExtraTokens_ParseToValues(string tokens, string? title, string? language, bool isDefault = false, bool isForced = false, bool isHearingImpaired = false)
|
||||||
{
|
{
|
||||||
var path = "My.Video" + tokens + ".srt";
|
var path = "My.Video" + tokens + ".srt";
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user