mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-02-13 16:12:23 +00:00
Compare commits
177 Commits
feature/en
...
v10.9.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b25d6d1e48 | ||
|
|
cf59140276 | ||
|
|
cc4563a477 | ||
|
|
0d984b5162 | ||
|
|
279cba008b | ||
|
|
2b5d458456 | ||
|
|
f41efb3b2c | ||
|
|
0155293c64 | ||
|
|
b78efd6b1e | ||
|
|
bfcc09db8a | ||
|
|
a46c17e19f | ||
|
|
b0bb22b650 | ||
|
|
0c039145e5 | ||
|
|
2a3c904a9f | ||
|
|
7cbdb6708b | ||
|
|
7058db2b04 | ||
|
|
8f7df590cd | ||
|
|
0f67a5ba2f | ||
|
|
19fb00b5b7 | ||
|
|
8683253c6d | ||
|
|
918a36d564 | ||
|
|
57ae0b5796 | ||
|
|
262f7dd98f | ||
|
|
4714b3af67 | ||
|
|
0359035000 | ||
|
|
b14edb8876 | ||
|
|
47c5e0c2c7 | ||
|
|
8709d94783 | ||
|
|
23b1251393 | ||
|
|
484aea1cdb | ||
|
|
d1c00ba4ed | ||
|
|
b1a5fe2f55 | ||
|
|
d7ff6d023c | ||
|
|
253e95dcba | ||
|
|
c7ce1aa4c7 | ||
|
|
3d87885577 | ||
|
|
a7e2271845 | ||
|
|
2a02abee46 | ||
|
|
ab4315742f | ||
|
|
2ddb15c784 | ||
|
|
95c7d997c1 | ||
|
|
e2c909f50f | ||
|
|
a53ea029fa | ||
|
|
869dab2ba2 | ||
|
|
d2be2ee480 | ||
|
|
bc8ef94f0d | ||
|
|
ed1b880359 | ||
|
|
7221e7ca68 | ||
|
|
0392daa103 | ||
|
|
d602b6dbc5 | ||
|
|
b8a0cf6a9e | ||
|
|
26419c64f5 | ||
|
|
a71e2d9f0a | ||
|
|
cfe67ff17d | ||
|
|
78e3ee15f9 | ||
|
|
2cb74e3dd0 | ||
|
|
8e979bdb4b | ||
|
|
e099fd6141 | ||
|
|
35962bcc42 | ||
|
|
ae584beaac | ||
|
|
563033786f | ||
|
|
f8c7f36a34 | ||
|
|
4746c88633 | ||
|
|
b7d6bedbbb | ||
|
|
8db79c05dd | ||
|
|
8fa7ff647a | ||
|
|
d0336cd67e | ||
|
|
cfab4eb2fc | ||
|
|
5f1c5009d3 | ||
|
|
97d7151289 | ||
|
|
475fa36ea3 | ||
|
|
e8d1ee0934 | ||
|
|
d07ec4ad0f | ||
|
|
684dfedbcc | ||
|
|
730b01fb14 | ||
|
|
45e8872cc0 | ||
|
|
cc2c00d764 | ||
|
|
402a5e2c9f | ||
|
|
bcf884ccfa | ||
|
|
2eece01acc | ||
|
|
ef985896e2 | ||
|
|
5e7514243c | ||
|
|
b9c0fc69e8 | ||
|
|
4a344bebc0 | ||
|
|
b2d54b82fa | ||
|
|
e7b1162cb3 | ||
|
|
d89e5a0074 | ||
|
|
4a54e5ddeb | ||
|
|
d9232e05f1 | ||
|
|
52be8be28f | ||
|
|
ab6c2424db | ||
|
|
eb437e7163 | ||
|
|
2ddf2a7866 | ||
|
|
60232ce9be | ||
|
|
952995f796 | ||
|
|
933a285bf5 | ||
|
|
f8da69f8e5 | ||
|
|
d5e29bfce3 | ||
|
|
ab36c4c011 | ||
|
|
c6e29647fc | ||
|
|
95a6291c34 | ||
|
|
fa2bff30f6 | ||
|
|
58041e1f9d | ||
|
|
447f73caf4 | ||
|
|
9145be6bfc | ||
|
|
86129589ef | ||
|
|
f5a8fca22f | ||
|
|
2a612611b8 | ||
|
|
0b64426cf2 | ||
|
|
f3bf9bcdc8 | ||
|
|
06a5ddda5e | ||
|
|
8a5a93ee80 | ||
|
|
6777f47e0e | ||
|
|
cf04e1d8e5 | ||
|
|
d608f1e3cc | ||
|
|
86f5c93434 | ||
|
|
4fcbeef5e6 | ||
|
|
7d983ae0dd | ||
|
|
a2ab34ef4c | ||
|
|
77abafca8e | ||
|
|
e67eb48540 | ||
|
|
37d7e8f5bf | ||
|
|
e6eef8bece | ||
|
|
52cfd9f261 | ||
|
|
9a9e8e2648 | ||
|
|
2cebd5e05f | ||
|
|
7fa72260ca | ||
|
|
b19b346670 | ||
|
|
99de0ca45f | ||
|
|
c106b399d7 | ||
|
|
c274062e87 | ||
|
|
53de8c0805 | ||
|
|
76854b5eff | ||
|
|
5200633574 | ||
|
|
832e27a8fb | ||
|
|
2da06bc0b1 | ||
|
|
46c748d888 | ||
|
|
430d450828 | ||
|
|
02937873b1 | ||
|
|
d303ca56e3 | ||
|
|
c647143e53 | ||
|
|
dd0ab8ed56 | ||
|
|
18e6c1ef7d | ||
|
|
dec2032e13 | ||
|
|
287e06d6dc | ||
|
|
dc93cc13b5 | ||
|
|
26714e2c62 | ||
|
|
c6c48a2b47 | ||
|
|
f8b67ec44c | ||
|
|
ddd5c302b4 | ||
|
|
9b98638b2b | ||
|
|
2ca8ce6f60 | ||
|
|
56a158e5c9 | ||
|
|
9b65d243a8 | ||
|
|
c45dd5d6fb | ||
|
|
af4b732080 | ||
|
|
1cdf0f5cc4 | ||
|
|
20a1da1855 | ||
|
|
8aee50020b | ||
|
|
c1615419b9 | ||
|
|
15489eeae3 | ||
|
|
80c9589885 | ||
|
|
a5d60c4521 | ||
|
|
2cb052a119 | ||
|
|
0756174b13 | ||
|
|
3f760e6685 | ||
|
|
d5dc4435d9 | ||
|
|
48228430c0 | ||
|
|
f396a95f05 | ||
|
|
717afcdc82 | ||
|
|
25c50bcc5d | ||
|
|
f77a5d0c5c | ||
|
|
6689d837d6 | ||
|
|
c1907354e8 | ||
|
|
efba619acb | ||
|
|
7d271547c6 | ||
|
|
327f92bb2e |
@@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
"version": "8.0.6",
|
"version": "8.0.4",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-ef"
|
"dotnet-ef"
|
||||||
]
|
]
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE/issue report.yml
vendored
7
.github/ISSUE_TEMPLATE/issue report.yml
vendored
@@ -38,11 +38,10 @@ body:
|
|||||||
label: Jellyfin Version
|
label: Jellyfin Version
|
||||||
description: What version of Jellyfin are you running?
|
description: What version of Jellyfin are you running?
|
||||||
options:
|
options:
|
||||||
- 10.9.0
|
|
||||||
- 10.8.13
|
- 10.8.13
|
||||||
- 10.8.12 or older (please specify)
|
- 10.8.12
|
||||||
- Weekly unstable (please specify)
|
- 10.8.11 or older (please specify)
|
||||||
- Master branch
|
- Unstable (master branch)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
|
|||||||
6
.github/workflows/ci-codeql-analysis.yml
vendored
6
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -27,11 +27,11 @@ jobs:
|
|||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
|
uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
|
uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@f079b8493333aace61c81488f8bd40919487bd9f # v3.25.7
|
uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||||
|
|||||||
2
.github/workflows/ci-tests.yml
vendored
2
.github/workflows/ci-tests.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
--verbosity minimal
|
--verbosity minimal
|
||||||
|
|
||||||
- name: Merge code coverage results
|
- name: Merge code coverage results
|
||||||
uses: danielpalme/ReportGenerator-GitHub-Action@fa728091745cdd279fddda1e0e80fb29265d0977 # 5.3.5
|
uses: danielpalme/ReportGenerator-GitHub-Action@6b06171d1a131e7fd85121120a1c00c1ed03e033 # 5.3.0
|
||||||
with:
|
with:
|
||||||
reports: "**/coverage.cobertura.xml"
|
reports: "**/coverage.cobertura.xml"
|
||||||
targetdir: "merged/"
|
targetdir: "merged/"
|
||||||
|
|||||||
2
.github/workflows/pull-request-conflict.yml
vendored
2
.github/workflows/pull-request-conflict.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Apply label
|
- name: Apply label
|
||||||
uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
|
uses: eps1lon/actions-label-merge-conflict@6d74047dcef155976a15e4a124dde2c7fe0c5522 # v3.0.1
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
||||||
with:
|
with:
|
||||||
dirtyLabel: 'merge conflict'
|
dirtyLabel: 'merge conflict'
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
|
||||||
|
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.6" />
|
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.6" />
|
||||||
@@ -73,7 +74,7 @@
|
|||||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||||
<PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
|
<PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.6.2" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||||
@@ -84,8 +85,8 @@
|
|||||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.8" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.1" />
|
||||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||||
<PackageVersion Include="xunit" Version="2.7.1" />
|
<PackageVersion Include="xunit" Version="2.8.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.10.0</VersionPrefix>
|
<VersionPrefix>10.9.6</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -13,6 +14,8 @@ namespace Emby.Server.Implementations.Data
|
|||||||
public abstract class BaseSqliteRepository : IDisposable
|
public abstract class BaseSqliteRepository : IDisposable
|
||||||
{
|
{
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
private SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
|
||||||
|
private SqliteConnection _writeConnection;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
|
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
|
||||||
@@ -98,9 +101,55 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected SqliteConnection GetConnection()
|
protected ManagedConnection GetConnection(bool readOnly = false)
|
||||||
{
|
{
|
||||||
var connection = new SqliteConnection($"Filename={DbFilePath}");
|
if (!readOnly)
|
||||||
|
{
|
||||||
|
_writeLock.Wait();
|
||||||
|
if (_writeConnection is not null)
|
||||||
|
{
|
||||||
|
return new ManagedConnection(_writeConnection, _writeLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
var writeConnection = new SqliteConnection($"Filename={DbFilePath};Pooling=False");
|
||||||
|
writeConnection.Open();
|
||||||
|
|
||||||
|
if (CacheSize.HasValue)
|
||||||
|
{
|
||||||
|
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(LockingMode))
|
||||||
|
{
|
||||||
|
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(JournalMode))
|
||||||
|
{
|
||||||
|
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JournalSizeLimit.HasValue)
|
||||||
|
{
|
||||||
|
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Synchronous.HasValue)
|
||||||
|
{
|
||||||
|
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PageSize.HasValue)
|
||||||
|
{
|
||||||
|
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||||
|
|
||||||
|
return new ManagedConnection(_writeConnection = writeConnection, _writeLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
var connection = new SqliteConnection($"Filename={DbFilePath};Mode=ReadOnly");
|
||||||
connection.Open();
|
connection.Open();
|
||||||
|
|
||||||
if (CacheSize.HasValue)
|
if (CacheSize.HasValue)
|
||||||
@@ -135,17 +184,17 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||||
|
|
||||||
return connection;
|
return new ManagedConnection(connection, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
|
public SqliteCommand PrepareStatement(ManagedConnection connection, string sql)
|
||||||
{
|
{
|
||||||
var command = connection.CreateCommand();
|
var command = connection.CreateCommand();
|
||||||
command.CommandText = sql;
|
command.CommandText = sql;
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected bool TableExists(SqliteConnection connection, string name)
|
protected bool TableExists(ManagedConnection connection, string name)
|
||||||
{
|
{
|
||||||
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
|
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
|
||||||
foreach (var row in statement.ExecuteQuery())
|
foreach (var row in statement.ExecuteQuery())
|
||||||
@@ -159,7 +208,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<string> GetColumnNames(SqliteConnection connection, string table)
|
protected List<string> GetColumnNames(ManagedConnection connection, string table)
|
||||||
{
|
{
|
||||||
var columnNames = new List<string>();
|
var columnNames = new List<string>();
|
||||||
|
|
||||||
@@ -174,7 +223,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return columnNames;
|
return columnNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
protected void AddColumn(ManagedConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
||||||
{
|
{
|
||||||
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -207,6 +256,24 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dispose)
|
||||||
|
{
|
||||||
|
_writeLock.Wait();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_writeConnection.Dispose();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_writeLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeLock.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeConnection = null;
|
||||||
|
_writeLock = null;
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
Emby.Server.Implementations/Data/ManagedConnection.cs
Normal file
62
Emby.Server.Implementations/Data/ManagedConnection.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations.Data;
|
||||||
|
|
||||||
|
public sealed class ManagedConnection : IDisposable
|
||||||
|
{
|
||||||
|
private readonly SemaphoreSlim? _writeLock;
|
||||||
|
|
||||||
|
private SqliteConnection _db;
|
||||||
|
|
||||||
|
private bool _disposed = false;
|
||||||
|
|
||||||
|
public ManagedConnection(SqliteConnection db, SemaphoreSlim? writeLock)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_writeLock = writeLock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SqliteTransaction BeginTransaction()
|
||||||
|
=> _db.BeginTransaction();
|
||||||
|
|
||||||
|
public SqliteCommand CreateCommand()
|
||||||
|
=> _db.CreateCommand();
|
||||||
|
|
||||||
|
public void Execute(string commandText)
|
||||||
|
=> _db.Execute(commandText);
|
||||||
|
|
||||||
|
public SqliteCommand PrepareStatement(string sql)
|
||||||
|
=> _db.PrepareStatement(sql);
|
||||||
|
|
||||||
|
public IEnumerable<SqliteDataReader> Query(string commandText)
|
||||||
|
=> _db.Query(commandText);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_writeLock is null)
|
||||||
|
{
|
||||||
|
// Read connections are managed with an internal pool
|
||||||
|
_db.Dispose();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Write lock is managed by BaseSqliteRepository
|
||||||
|
// Don't dispose here
|
||||||
|
_writeLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
_db = null!;
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -601,7 +601,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveItemsInTransaction(SqliteConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
|
private void SaveItemsInTransaction(ManagedConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
|
||||||
{
|
{
|
||||||
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
|
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
|
||||||
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
|
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
|
||||||
@@ -1261,7 +1261,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
|
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
|
||||||
{
|
{
|
||||||
statement.TryBind("@guid", id);
|
statement.TryBind("@guid", id);
|
||||||
@@ -1887,7 +1887,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
var chapters = new List<ChapterInfo>();
|
var chapters = new List<ChapterInfo>();
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
|
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
|
||||||
{
|
{
|
||||||
statement.TryBind("@ItemId", item.Id);
|
statement.TryBind("@ItemId", item.Id);
|
||||||
@@ -1906,7 +1906,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
|
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
|
||||||
{
|
{
|
||||||
statement.TryBind("@ItemId", item.Id);
|
statement.TryBind("@ItemId", item.Id);
|
||||||
@@ -1980,7 +1980,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, SqliteConnection db)
|
private void InsertChapters(Guid idBlob, IReadOnlyList<ChapterInfo> chapters, ManagedConnection db)
|
||||||
{
|
{
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
var limit = 100;
|
var limit = 100;
|
||||||
@@ -2469,7 +2469,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var commandText = commandTextBuilder.ToString();
|
var commandText = commandTextBuilder.ToString();
|
||||||
|
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, commandText))
|
using (var statement = PrepareStatement(connection, commandText))
|
||||||
{
|
{
|
||||||
if (EnableJoinUserData(query))
|
if (EnableJoinUserData(query))
|
||||||
@@ -2537,7 +2537,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var commandText = commandTextBuilder.ToString();
|
var commandText = commandTextBuilder.ToString();
|
||||||
var items = new List<BaseItem>();
|
var items = new List<BaseItem>();
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, commandText))
|
using (var statement = PrepareStatement(connection, commandText))
|
||||||
{
|
{
|
||||||
if (EnableJoinUserData(query))
|
if (EnableJoinUserData(query))
|
||||||
@@ -2745,7 +2745,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
var list = new List<BaseItem>();
|
var list = new List<BaseItem>();
|
||||||
var result = new QueryResult<BaseItem>();
|
var result = new QueryResult<BaseItem>();
|
||||||
using var connection = GetConnection();
|
using var connection = GetConnection(true);
|
||||||
using var transaction = connection.BeginTransaction();
|
using var transaction = connection.BeginTransaction();
|
||||||
if (!isReturningZeroItems)
|
if (!isReturningZeroItems)
|
||||||
{
|
{
|
||||||
@@ -2927,7 +2927,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var commandText = commandTextBuilder.ToString();
|
var commandText = commandTextBuilder.ToString();
|
||||||
var list = new List<Guid>();
|
var list = new List<Guid>();
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, commandText))
|
using (var statement = PrepareStatement(connection, commandText))
|
||||||
{
|
{
|
||||||
if (EnableJoinUserData(query))
|
if (EnableJoinUserData(query))
|
||||||
@@ -4476,7 +4476,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteWithSingleParam(SqliteConnection db, string query, Guid value)
|
private void ExecuteWithSingleParam(ManagedConnection db, string query, Guid value)
|
||||||
{
|
{
|
||||||
using (var statement = PrepareStatement(db, query))
|
using (var statement = PrepareStatement(db, query))
|
||||||
{
|
{
|
||||||
@@ -4509,7 +4509,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<string>();
|
var list = new List<string>();
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
||||||
{
|
{
|
||||||
// Run this again to bind the params
|
// Run this again to bind the params
|
||||||
@@ -4547,7 +4547,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<PersonInfo>();
|
var list = new List<PersonInfo>();
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
||||||
{
|
{
|
||||||
// Run this again to bind the params
|
// Run this again to bind the params
|
||||||
@@ -4632,7 +4632,7 @@ AND Type = @InternalPersonType)");
|
|||||||
return whereClauses;
|
return whereClauses;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement)
|
private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, ManagedConnection db, SqliteCommand deleteAncestorsStatement)
|
||||||
{
|
{
|
||||||
if (itemId.IsEmpty())
|
if (itemId.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -4787,7 +4787,7 @@ AND Type = @InternalPersonType)");
|
|||||||
|
|
||||||
var list = new List<string>();
|
var list = new List<string>();
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, commandText))
|
using (var statement = PrepareStatement(connection, commandText))
|
||||||
{
|
{
|
||||||
foreach (var row in statement.ExecuteQuery())
|
foreach (var row in statement.ExecuteQuery())
|
||||||
@@ -4987,8 +4987,8 @@ AND Type = @InternalPersonType)");
|
|||||||
var list = new List<(BaseItem, ItemCounts)>();
|
var list = new List<(BaseItem, ItemCounts)>();
|
||||||
var result = new QueryResult<(BaseItem, ItemCounts)>();
|
var result = new QueryResult<(BaseItem, ItemCounts)>();
|
||||||
using (new QueryTimeLogger(Logger, commandText))
|
using (new QueryTimeLogger(Logger, commandText))
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var transaction = connection.BeginTransaction(deferred: true))
|
using (var transaction = connection.BeginTransaction())
|
||||||
{
|
{
|
||||||
if (!isReturningZeroItems)
|
if (!isReturningZeroItems)
|
||||||
{
|
{
|
||||||
@@ -5148,7 +5148,7 @@ AND Type = @InternalPersonType)");
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db)
|
private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, ManagedConnection db)
|
||||||
{
|
{
|
||||||
if (itemId.IsEmpty())
|
if (itemId.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -5167,7 +5167,7 @@ AND Type = @InternalPersonType)");
|
|||||||
InsertItemValues(itemId, values, db);
|
InsertItemValues(itemId, values, db);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, SqliteConnection db)
|
private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, ManagedConnection db)
|
||||||
{
|
{
|
||||||
const int Limit = 100;
|
const int Limit = 100;
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
@@ -5239,7 +5239,7 @@ AND Type = @InternalPersonType)");
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertPeople(Guid id, List<PersonInfo> people, SqliteConnection db)
|
private void InsertPeople(Guid id, List<PersonInfo> people, ManagedConnection db)
|
||||||
{
|
{
|
||||||
const int Limit = 100;
|
const int Limit = 100;
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
@@ -5335,7 +5335,7 @@ AND Type = @InternalPersonType)");
|
|||||||
|
|
||||||
cmdText += " order by StreamIndex ASC";
|
cmdText += " order by StreamIndex ASC";
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
{
|
{
|
||||||
var list = new List<MediaStream>();
|
var list = new List<MediaStream>();
|
||||||
|
|
||||||
@@ -5388,7 +5388,7 @@ AND Type = @InternalPersonType)");
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, SqliteConnection db)
|
private void InsertMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, ManagedConnection db)
|
||||||
{
|
{
|
||||||
const int Limit = 10;
|
const int Limit = 10;
|
||||||
var startIndex = 0;
|
var startIndex = 0;
|
||||||
@@ -5722,7 +5722,7 @@ AND Type = @InternalPersonType)");
|
|||||||
cmdText += " order by AttachmentIndex ASC";
|
cmdText += " order by AttachmentIndex ASC";
|
||||||
|
|
||||||
var list = new List<MediaAttachment>();
|
var list = new List<MediaAttachment>();
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
using (var statement = PrepareStatement(connection, cmdText))
|
using (var statement = PrepareStatement(connection, cmdText))
|
||||||
{
|
{
|
||||||
statement.TryBind("@ItemId", query.ItemId);
|
statement.TryBind("@ItemId", query.ItemId);
|
||||||
@@ -5772,7 +5772,7 @@ AND Type = @InternalPersonType)");
|
|||||||
private void InsertMediaAttachments(
|
private void InsertMediaAttachments(
|
||||||
Guid id,
|
Guid id,
|
||||||
IReadOnlyList<MediaAttachment> attachments,
|
IReadOnlyList<MediaAttachment> attachments,
|
||||||
SqliteConnection db,
|
ManagedConnection db,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
const int InsertAtOnce = 10;
|
const int InsertAtOnce = 10;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
|
private void ImportUserIds(ManagedConnection db, IEnumerable<User> users)
|
||||||
{
|
{
|
||||||
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
|
private List<Guid> GetAllUserIdsWithUserData(ManagedConnection db)
|
||||||
{
|
{
|
||||||
var list = new List<Guid>();
|
var list = new List<Guid>();
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
|
private static void SaveUserData(ManagedConnection db, long internalUserId, string key, UserItemData userData)
|
||||||
{
|
{
|
||||||
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
|
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
|
||||||
{
|
{
|
||||||
@@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection(true))
|
||||||
{
|
{
|
||||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1029,7 +1029,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
|
||||||
{
|
{
|
||||||
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
IndexNumber = seasonParserResult.SeasonNumber,
|
IndexNumber = seasonParserResult.SeasonNumber,
|
||||||
SeriesId = series.Id,
|
SeriesId = series.Id,
|
||||||
SeriesName = series.Name,
|
SeriesName = series.Name,
|
||||||
Path = seasonParserResult.IsSeasonFolder ? path : args.Parent.Path
|
Path = seasonParserResult.IsSeasonFolder ? path : null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
|
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
{
|
{}
|
||||||
"Albums": "аальбомқәа"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -127,7 +127,5 @@
|
|||||||
"TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
|
"TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.",
|
"TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання",
|
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і спісы прайгравання",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і спісаў прайгравання, якія больш не існуюць."
|
||||||
"TaskAudioNormalizationDescription": "Сканіруе файлы на прадмет нармалізацыі гуку.",
|
|
||||||
"TaskAudioNormalization": "Нармалізацыя гуку"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"HeaderFavoriteEpisodes": "Oblíbené epizody",
|
"HeaderFavoriteEpisodes": "Oblíbené epizody",
|
||||||
"HeaderFavoriteShows": "Oblíbené seriály",
|
"HeaderFavoriteShows": "Oblíbené seriály",
|
||||||
"HeaderFavoriteSongs": "Oblíbená hudba",
|
"HeaderFavoriteSongs": "Oblíbená hudba",
|
||||||
"HeaderLiveTV": "TV vysílání",
|
"HeaderLiveTV": "Živý přenos",
|
||||||
"HeaderNextUp": "Další díly",
|
"HeaderNextUp": "Další díly",
|
||||||
"HeaderRecordingGroups": "Skupiny nahrávek",
|
"HeaderRecordingGroups": "Skupiny nahrávek",
|
||||||
"HomeVideos": "Domácí videa",
|
"HomeVideos": "Domácí videa",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"Genres": "Genrer",
|
"Genres": "Genrer",
|
||||||
"HeaderAlbumArtists": "Albumkunstnere",
|
"HeaderAlbumArtists": "Albumkunstnere",
|
||||||
"HeaderContinueWatching": "Fortsæt afspilning",
|
"HeaderContinueWatching": "Fortsæt afspilning",
|
||||||
"HeaderFavoriteAlbums": "Favoritalbum",
|
"HeaderFavoriteAlbums": "Favoritalbummer",
|
||||||
"HeaderFavoriteArtists": "Favoritkunstnere",
|
"HeaderFavoriteArtists": "Favoritkunstnere",
|
||||||
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
|
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
|
||||||
"HeaderFavoriteShows": "Yndlingsserier",
|
"HeaderFavoriteShows": "Yndlingsserier",
|
||||||
@@ -87,21 +87,21 @@
|
|||||||
"UserOnlineFromDevice": "{0} er online fra {1}",
|
"UserOnlineFromDevice": "{0} er online fra {1}",
|
||||||
"UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
|
"UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
|
||||||
"UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
|
"UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
|
||||||
"UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1} på {2}",
|
"UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}",
|
||||||
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
|
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
|
||||||
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
|
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
|
||||||
"ValueSpecialEpisodeName": "Special - {0}",
|
"ValueSpecialEpisodeName": "Special - {0}",
|
||||||
"VersionNumber": "Version {0}",
|
"VersionNumber": "Version {0}",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
|
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
|
||||||
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
|
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
|
||||||
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er konfigurerede til at blive opdateret automatisk.",
|
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
|
||||||
"TaskUpdatePlugins": "Opdater Plugins",
|
"TaskUpdatePlugins": "Opdater Plugins",
|
||||||
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
|
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
|
||||||
"TaskCleanLogs": "Ryd Log-mappe",
|
"TaskCleanLogs": "Ryd Log-mappe",
|
||||||
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
|
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
|
||||||
"TaskRefreshLibrary": "Scan Mediebibliotek",
|
"TaskRefreshLibrary": "Scan Mediebibliotek",
|
||||||
"TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
|
"TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
|
||||||
"TaskCleanCache": "Ryd cache-mappe",
|
"TaskCleanCache": "Ryd Cache-mappe",
|
||||||
"TasksChannelsCategory": "Internetkanaler",
|
"TasksChannelsCategory": "Internetkanaler",
|
||||||
"TasksApplicationCategory": "Applikation",
|
"TasksApplicationCategory": "Applikation",
|
||||||
"TasksLibraryCategory": "Bibliotek",
|
"TasksLibraryCategory": "Bibliotek",
|
||||||
@@ -128,7 +128,5 @@
|
|||||||
"TaskRefreshTrickplayImages": "Generér Trickplay Billeder",
|
"TaskRefreshTrickplayImages": "Generér Trickplay Billeder",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker.",
|
"TaskRefreshTrickplayImagesDescription": "Laver trickplay forhåndsvisninger for videoer i aktiverede biblioteker.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
|
"TaskCleanCollectionsAndPlaylists": "Ryd op i samlinger og afspilningslister",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra samlinger og afspilningslister der ikke eksisterer længere.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Fjerner enheder fra samlinger og afspilningslister der ikke eksisterer længere."
|
||||||
"TaskAudioNormalizationDescription": "Skanner filer for data vedrørende audio-normalisering.",
|
|
||||||
"TaskAudioNormalization": "Audio-normalisering"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,9 +126,5 @@
|
|||||||
"External": "Εξωτερικό",
|
"External": "Εξωτερικό",
|
||||||
"HearingImpaired": "Με προβλήματα ακοής",
|
"HearingImpaired": "Με προβλήματα ακοής",
|
||||||
"TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
|
"TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
|
"TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες."
|
||||||
"TaskAudioNormalization": "Ομοιομορφία ήχου",
|
|
||||||
"TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
|
|
||||||
"TaskCleanCollectionsAndPlaylists": "Καθαρισμός συλλογών και λιστών αναπαραγωγής",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Αφαιρούνται στοιχεία από τις συλλογές και τις λίστες αναπαραγωγής που δεν υπάρχουν πλέον."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Colecciones",
|
"Collections": "Colecciones",
|
||||||
"DeviceOfflineWithName": "{0} se ha desconectado",
|
"DeviceOfflineWithName": "{0} se ha desconectado",
|
||||||
"DeviceOnlineWithName": "{0} está conectado",
|
"DeviceOnlineWithName": "{0} está conectado",
|
||||||
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
|
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
|
||||||
"Favorites": "Favoritos",
|
"Favorites": "Favoritos",
|
||||||
"Folders": "Carpetas",
|
"Folders": "Carpetas",
|
||||||
"Genres": "Géneros",
|
"Genres": "Géneros",
|
||||||
@@ -124,11 +124,5 @@
|
|||||||
"TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.",
|
"TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.",
|
||||||
"TaskKeyframeExtractor": "Extractor de Cuadros Clave",
|
"TaskKeyframeExtractor": "Extractor de Cuadros Clave",
|
||||||
"External": "Externo",
|
"External": "Externo",
|
||||||
"HearingImpaired": "Discapacidad Auditiva",
|
"HearingImpaired": "Discapacidad Auditiva"
|
||||||
"TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.",
|
|
||||||
"TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción",
|
|
||||||
"TaskAudioNormalization": "Normalización de audio",
|
|
||||||
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
|
|
||||||
"TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Colecciones",
|
"Collections": "Colecciones",
|
||||||
"DeviceOfflineWithName": "{0} se ha desconectado",
|
"DeviceOfflineWithName": "{0} se ha desconectado",
|
||||||
"DeviceOnlineWithName": "{0} está conectado",
|
"DeviceOnlineWithName": "{0} está conectado",
|
||||||
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
|
"FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión desde {0}",
|
||||||
"Favorites": "Favoritos",
|
"Favorites": "Favoritos",
|
||||||
"Folders": "Carpetas",
|
"Folders": "Carpetas",
|
||||||
"Genres": "Géneros",
|
"Genres": "Géneros",
|
||||||
|
|||||||
@@ -12,118 +12,14 @@
|
|||||||
"Application": "Aplicación",
|
"Application": "Aplicación",
|
||||||
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
|
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
|
||||||
"HeaderContinueWatching": "Continuar Viendo",
|
"HeaderContinueWatching": "Continuar Viendo",
|
||||||
"HeaderAlbumArtists": "Artistas del álbum",
|
"HeaderAlbumArtists": "Artistas del Álbum",
|
||||||
"Genres": "Géneros",
|
"Genres": "Géneros",
|
||||||
"Folders": "Carpetas",
|
"Folders": "Carpetas",
|
||||||
"Favorites": "Favoritos",
|
"Favorites": "Favoritos",
|
||||||
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
|
"FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}",
|
||||||
"HeaderFavoriteSongs": "Canciones Favoritas",
|
"HeaderFavoriteSongs": "Canciones Favoritas",
|
||||||
"HeaderFavoriteEpisodes": "Episodios Favoritos",
|
"HeaderFavoriteEpisodes": "Episodios Favoritos",
|
||||||
"HeaderFavoriteArtists": "Artistas Favoritos",
|
"HeaderFavoriteArtists": "Artistas Favoritos",
|
||||||
"External": "Externo",
|
"External": "Externo",
|
||||||
"Default": "Predeterminado",
|
"Default": "Predeterminado"
|
||||||
"Movies": "Películas",
|
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de la configuración ha sido actualizada",
|
|
||||||
"MixedContent": "Contenido mixto",
|
|
||||||
"Music": "Música",
|
|
||||||
"NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
|
|
||||||
"NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
|
|
||||||
"NotificationOptionVideoPlayback": "Reproducción de video iniciada",
|
|
||||||
"Sync": "Sincronizar",
|
|
||||||
"Shows": "Series",
|
|
||||||
"UserDownloadingItemWithValues": "{0} está descargando {1}",
|
|
||||||
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
|
|
||||||
"UserOnlineFromDevice": "{0} está en línea desde {1}",
|
|
||||||
"TasksChannelsCategory": "Canales de Internet",
|
|
||||||
"TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
|
|
||||||
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
|
|
||||||
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y libera espacio. Ejecutar esta tarea después de escanear la biblioteca o hacer otros cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.",
|
|
||||||
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
|
|
||||||
"TaskAudioNormalization": "Normalización de audio",
|
|
||||||
"TaskAudioNormalizationDescription": "Escanear archivos para la normalización de data.",
|
|
||||||
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remover elementos de colecciones y listas de reproducción que no existen.",
|
|
||||||
"TvShows": "Series de TV",
|
|
||||||
"UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
|
|
||||||
"TaskRefreshChannels": "Actualizar canales",
|
|
||||||
"Photos": "Fotos",
|
|
||||||
"HeaderFavoriteShows": "Programas favoritos",
|
|
||||||
"TaskCleanActivityLog": "Limpiar registro de actividades",
|
|
||||||
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
|
|
||||||
"System": "Sistema",
|
|
||||||
"User": "Usuario",
|
|
||||||
"Forced": "Forzado",
|
|
||||||
"PluginInstalledWithName": "{0} ha sido instalado",
|
|
||||||
"HeaderFavoriteAlbums": "Álbumes favoritos",
|
|
||||||
"TaskUpdatePlugins": "Actualizar Plugins",
|
|
||||||
"Latest": "Recientes",
|
|
||||||
"UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
|
|
||||||
"Songs": "Canciones",
|
|
||||||
"NotificationOptionPluginError": "Falla de plugin",
|
|
||||||
"ScheduledTaskStartedWithName": "{0} iniciado",
|
|
||||||
"TasksApplicationCategory": "Aplicación",
|
|
||||||
"UserDeletedWithName": "El usuario {0} ha sido eliminado",
|
|
||||||
"TaskRefreshChapterImages": "Extraer imágenes de los capítulos",
|
|
||||||
"TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para plugins que están configurados para actualizarse automáticamente.",
|
|
||||||
"TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
|
|
||||||
"NotificationOptionUserLockedOut": "Usuario bloqueado",
|
|
||||||
"TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
|
|
||||||
"TaskCleanTranscode": "Limpiar el directorio de transcodificaciones",
|
|
||||||
"NotificationOptionPluginUpdateInstalled": "Actualización de plugin instalada",
|
|
||||||
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
|
|
||||||
"TasksLibraryCategory": "Biblioteca",
|
|
||||||
"NotificationOptionPluginInstalled": "Plugin instalado",
|
|
||||||
"UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
|
|
||||||
"VersionNumber": "Versión {0}",
|
|
||||||
"HeaderNextUp": "A continuación",
|
|
||||||
"ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca",
|
|
||||||
"LabelIpAddressValue": "Dirección IP: {0}",
|
|
||||||
"NameSeasonNumber": "Temporada {0}",
|
|
||||||
"NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
|
|
||||||
"Plugin": "Plugin",
|
|
||||||
"NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
|
|
||||||
"NotificationOptionTaskFailed": "Falló la tarea programada",
|
|
||||||
"LabelRunningTimeValue": "Tiempo en ejecución: {0}",
|
|
||||||
"SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
|
|
||||||
"TaskRefreshLibrary": "Escanear biblioteca de medios",
|
|
||||||
"ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
|
|
||||||
"TasksMaintenanceCategory": "Mantenimiento",
|
|
||||||
"ProviderValue": "Proveedor: {0}",
|
|
||||||
"UserCreatedWithName": "El usuario {0} ha sido creado",
|
|
||||||
"PluginUninstalledWithName": "{0} ha sido desinstalado",
|
|
||||||
"ValueSpecialEpisodeName": "Especial - {0}",
|
|
||||||
"ScheduledTaskFailedWithName": "{0} falló",
|
|
||||||
"TaskCleanLogs": "Limpiar directorio de registros",
|
|
||||||
"NameInstallFailed": "Falló la instalación de {0}",
|
|
||||||
"UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
|
|
||||||
"TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios para encontrar archivos nuevos y actualizar los metadatos.",
|
|
||||||
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo en un momento.",
|
|
||||||
"Playlists": "Listas de reproducción",
|
|
||||||
"TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
|
|
||||||
"MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
|
|
||||||
"TaskRefreshPeople": "Actualizar personas",
|
|
||||||
"NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida",
|
|
||||||
"HeaderLiveTV": "TV en vivo",
|
|
||||||
"NameSeasonUnknown": "Temporada desconocida",
|
|
||||||
"NotificationOptionInstallationFailed": "Fallo de instalación",
|
|
||||||
"NotificationOptionPluginUninstalled": "Plugin desinstalado",
|
|
||||||
"TaskCleanCache": "Limpiar directorio caché",
|
|
||||||
"TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.",
|
|
||||||
"Inherit": "Heredar",
|
|
||||||
"HeaderRecordingGroups": "Grupos de grabación",
|
|
||||||
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
|
|
||||||
"TaskOptimizeDatabase": "Optimizar base de datos",
|
|
||||||
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
|
|
||||||
"HearingImpaired": "Discapacidad auditiva",
|
|
||||||
"HomeVideos": "Videos caseros",
|
|
||||||
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
|
|
||||||
"MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
|
|
||||||
"MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",
|
|
||||||
"MusicVideos": "Videos musicales",
|
|
||||||
"NewVersionIsAvailable": "Una nueva versión de Jellyfin está disponible para descargar.",
|
|
||||||
"PluginUpdatedWithName": "{0} ha sido actualizado",
|
|
||||||
"Undefined": "Sin definir",
|
|
||||||
"TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
|
|
||||||
"TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.",
|
|
||||||
"TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,5 @@
|
|||||||
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
|
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
|
||||||
"TaskKeyframeExtractor": "Võtmekaadri ekstraktor",
|
"TaskKeyframeExtractor": "Võtmekaadri ekstraktor",
|
||||||
"TaskRefreshTrickplayImages": "Loo eelvaate pildid",
|
"TaskRefreshTrickplayImages": "Loo eelvaate pildid",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud.",
|
"TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud."
|
||||||
"TaskAudioNormalization": "Heli Normaliseerimine",
|
|
||||||
"TaskAudioNormalizationDescription": "Skaneerib faile heli normaliseerimise andmete jaoks."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,5 @@
|
|||||||
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
|
"TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
|
"TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista.",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Poistaa kohteet kokoelmista ja soittolistoista joita ei ole enää olemassa.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat",
|
"TaskCleanCollectionsAndPlaylists": "Puhdista kokoelmat ja soittolistat"
|
||||||
"TaskAudioNormalization": "Äänenvoimakkuuden normalisointi",
|
|
||||||
"TaskAudioNormalizationDescription": "Etsii tiedostoista äänenvoimakkuuden normalisointitietoja."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Collections",
|
"Collections": "Collections",
|
||||||
"DeviceOfflineWithName": "{0} s'est déconnecté",
|
"DeviceOfflineWithName": "{0} s'est déconnecté",
|
||||||
"DeviceOnlineWithName": "{0} est connecté",
|
"DeviceOnlineWithName": "{0} est connecté",
|
||||||
"FailedLoginAttemptWithUserName": "Tentative de connexion échouée par {0}",
|
"FailedLoginAttemptWithUserName": "Tentative de connexion échoué par {0}",
|
||||||
"Favorites": "Favoris",
|
"Favorites": "Favoris",
|
||||||
"Folders": "Dossiers",
|
"Folders": "Dossiers",
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"MixedContent": "Contenu mixte",
|
"MixedContent": "Contenu mixte",
|
||||||
"Movies": "Films",
|
"Movies": "Films",
|
||||||
"Music": "Musique",
|
"Music": "Musique",
|
||||||
"MusicVideos": "Vidéoclips",
|
"MusicVideos": "Vidéos musicales",
|
||||||
"NameInstallFailed": "échec d'installation de {0}",
|
"NameInstallFailed": "échec d'installation de {0}",
|
||||||
"NameSeasonNumber": "Saison {0}",
|
"NameSeasonNumber": "Saison {0}",
|
||||||
"NameSeasonUnknown": "Saison Inconnue",
|
"NameSeasonUnknown": "Saison Inconnue",
|
||||||
@@ -128,7 +128,5 @@
|
|||||||
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
|
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Supprimer les liens inexistants des collections et des listes de lecture"
|
||||||
"TaskAudioNormalization": "Normalisation audio",
|
|
||||||
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,9 +126,5 @@
|
|||||||
"External": "חיצוני",
|
"External": "חיצוני",
|
||||||
"HearingImpaired": "לקוי שמיעה",
|
"HearingImpaired": "לקוי שמיעה",
|
||||||
"TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
|
"TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
|
||||||
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות.",
|
"TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות."
|
||||||
"TaskAudioNormalization": "נרמול שמע",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
|
|
||||||
"TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
|
|
||||||
"TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
"Movies": "Film",
|
"Movies": "Film",
|
||||||
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
|
"MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
|
"MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
|
||||||
"FailedLoginAttemptWithUserName": "Gagal upaya login dari {0}",
|
"FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}",
|
||||||
"CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
|
"CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
|
||||||
"DeviceOfflineWithName": "{0} telah terputus",
|
"DeviceOfflineWithName": "{0} telah terputus",
|
||||||
"DeviceOnlineWithName": "{0} telah terhubung",
|
"DeviceOnlineWithName": "{0} telah terhubung",
|
||||||
@@ -125,9 +125,5 @@
|
|||||||
"External": "Luar",
|
"External": "Luar",
|
||||||
"HearingImpaired": "Gangguan Pendengaran",
|
"HearingImpaired": "Gangguan Pendengaran",
|
||||||
"TaskRefreshTrickplayImages": "Hasilkan Gambar Trickplay",
|
"TaskRefreshTrickplayImages": "Hasilkan Gambar Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan.",
|
"TaskRefreshTrickplayImagesDescription": "Buat pratinjau trickplay untuk video di perpustakaan yang diaktifkan."
|
||||||
"TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.",
|
|
||||||
"TaskAudioNormalization": "Normalisasi Audio",
|
|
||||||
"TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
"UserDeletedWithName": "L'utente {0} è stato rimosso",
|
"UserDeletedWithName": "L'utente {0} è stato rimosso",
|
||||||
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
|
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
|
||||||
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
|
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
|
||||||
"UserOfflineFromDevice": "{0} si è disconnesso da {1}",
|
"UserOfflineFromDevice": "{0} si è disconnesso su {1}",
|
||||||
"UserOnlineFromDevice": "{0} è online su {1}",
|
"UserOnlineFromDevice": "{0} è online su {1}",
|
||||||
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
|
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
|
||||||
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
|
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Collecties",
|
"Collections": "Collecties",
|
||||||
"DeviceOfflineWithName": "Verbinding met {0} is verbroken",
|
"DeviceOfflineWithName": "Verbinding met {0} is verbroken",
|
||||||
"DeviceOnlineWithName": "{0} is verbonden",
|
"DeviceOnlineWithName": "{0} is verbonden",
|
||||||
"FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
|
"FailedLoginAttemptWithUserName": "Mislukte inlogpoging van {0}",
|
||||||
"Favorites": "Favorieten",
|
"Favorites": "Favorieten",
|
||||||
"Folders": "Mappen",
|
"Folders": "Mappen",
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
|
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
|
||||||
"TaskKeyframeExtractor": "Keyframes uitpakken",
|
"TaskKeyframeExtractor": "Keyframes uitpakken",
|
||||||
"External": "Extern",
|
"External": "Extern",
|
||||||
"HearingImpaired": "Slechthorenden",
|
"HearingImpaired": "Slechthorend",
|
||||||
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
|
"TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.",
|
"TaskRefreshTrickplayImagesDescription": "Creëert trickplay-voorvertoningen voor video's in bibliotheken waarvoor dit is ingeschakeld.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
|
"TaskCleanCollectionsAndPlaylists": "Collecties en afspeellijsten opruimen",
|
||||||
|
|||||||
@@ -118,6 +118,5 @@
|
|||||||
"Undefined": "Udefinert",
|
"Undefined": "Udefinert",
|
||||||
"Forced": "Tvungen",
|
"Forced": "Tvungen",
|
||||||
"Default": "Standard",
|
"Default": "Standard",
|
||||||
"External": "Ekstern",
|
"External": "Ekstern"
|
||||||
"HearingImpaired": "Nedsett høyrsel"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Kolekcje",
|
"Collections": "Kolekcje",
|
||||||
"DeviceOfflineWithName": "{0} został rozłączony",
|
"DeviceOfflineWithName": "{0} został rozłączony",
|
||||||
"DeviceOnlineWithName": "{0} połączył się",
|
"DeviceOnlineWithName": "{0} połączył się",
|
||||||
"FailedLoginAttemptWithUserName": "Nieudana próba logowania przez {0}",
|
"FailedLoginAttemptWithUserName": "Próba logowania przez {0} zakończona niepowodzeniem",
|
||||||
"Favorites": "Ulubione",
|
"Favorites": "Ulubione",
|
||||||
"Folders": "Foldery",
|
"Folders": "Foldery",
|
||||||
"Genres": "Gatunki",
|
"Genres": "Gatunki",
|
||||||
@@ -98,8 +98,8 @@
|
|||||||
"TaskRefreshChannels": "Odśwież kanały",
|
"TaskRefreshChannels": "Odśwież kanały",
|
||||||
"TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
|
"TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
|
||||||
"TaskCleanTranscode": "Wyczyść folder transkodowania",
|
"TaskCleanTranscode": "Wyczyść folder transkodowania",
|
||||||
"TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje wtyczek, które są skonfigurowane do automatycznej aktualizacji.",
|
"TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów, które są skonfigurowane do automatycznej aktualizacji.",
|
||||||
"TaskUpdatePlugins": "Aktualizuj wtyczki",
|
"TaskUpdatePlugins": "Aktualizuj pluginy",
|
||||||
"TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
|
"TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
|
||||||
"TaskRefreshPeople": "Odśwież obsadę",
|
"TaskRefreshPeople": "Odśwież obsadę",
|
||||||
"TaskCleanLogsDescription": "Kasuje pliki logów starsze niż {0} dni.",
|
"TaskCleanLogsDescription": "Kasuje pliki logów starsze niż {0} dni.",
|
||||||
|
|||||||
@@ -130,5 +130,5 @@
|
|||||||
"TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists",
|
"TaskCleanCollectionsAndPlaylists": "Limpe coleções e playlists",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e playlists que não existem mais.",
|
||||||
"TaskAudioNormalization": "Normalização de áudio",
|
"TaskAudioNormalization": "Normalização de áudio",
|
||||||
"TaskAudioNormalizationDescription": "Examina os ficheiros em busca de dados de normalização de áudio."
|
"TaskAudioNormalizationDescription": "Verifica arquivos em busca de dados de normalização de áudio."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Коллекции",
|
"Collections": "Коллекции",
|
||||||
"DeviceOfflineWithName": "{0} - отключено",
|
"DeviceOfflineWithName": "{0} - отключено",
|
||||||
"DeviceOnlineWithName": "{0} - подключено",
|
"DeviceOnlineWithName": "{0} - подключено",
|
||||||
"FailedLoginAttemptWithUserName": "Неудачная попытка входа с {0}",
|
"FailedLoginAttemptWithUserName": "{0} - попытка входа неудачна",
|
||||||
"Favorites": "Избранное",
|
"Favorites": "Избранное",
|
||||||
"Folders": "Папки",
|
"Folders": "Папки",
|
||||||
"Genres": "Жанры",
|
"Genres": "Жанры",
|
||||||
@@ -128,7 +128,5 @@
|
|||||||
"TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
|
"TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.",
|
"TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения",
|
"TaskCleanCollectionsAndPlaylists": "Очистка коллекций и списков воспроизведения",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Удаляет элементы из коллекций и списков воспроизведения, которые больше не существуют."
|
||||||
"TaskAudioNormalization": "Нормализация звука",
|
|
||||||
"TaskAudioNormalizationDescription": "Сканирует файлы на наличие данных о нормализации звука."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,8 +127,5 @@
|
|||||||
"HearingImpaired": "Hörselskadad",
|
"HearingImpaired": "Hörselskadad",
|
||||||
"TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
|
"TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek.",
|
"TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Rensa upp samlingar och spellistor",
|
"TaskCleanCollectionsAndPlaylists": "Rensa samlingar och spellistor"
|
||||||
"TaskAudioNormalization": "Ljudnormalisering",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Tar bort objekt från samlingar och spellistor som inte längre finns.",
|
|
||||||
"TaskAudioNormalizationDescription": "Skannar filer för ljudnormaliseringsdata."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,9 +125,5 @@
|
|||||||
"External": "வெளி",
|
"External": "வெளி",
|
||||||
"HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
|
"HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
|
||||||
"TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
|
"TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
|
||||||
"TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்.",
|
"TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்."
|
||||||
"TaskCleanCollectionsAndPlaylists": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களை சுத்தம் செய்யவும்",
|
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "சேகரிப்புகள் மற்றும் பிளேலிஸ்ட்களில் இருந்து உருப்படிகளை நீக்குகிறது.",
|
|
||||||
"TaskAudioNormalization": "ஆடியோ இயல்பாக்கம்",
|
|
||||||
"TaskAudioNormalizationDescription": "ஆடியோ இயல்பாக்குதல் தரவுக்காக கோப்புகளை ஸ்கேன் செய்கிறது."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "Koleksiyonlar",
|
"Collections": "Koleksiyonlar",
|
||||||
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
|
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
|
||||||
"DeviceOnlineWithName": "{0} bağlı",
|
"DeviceOnlineWithName": "{0} bağlı",
|
||||||
"FailedLoginAttemptWithUserName": "{0} kullanıcısının başarısız oturum açma girişimi",
|
"FailedLoginAttemptWithUserName": "{0} kullanıcısının giriş denemesi başarısız oldu",
|
||||||
"Favorites": "Favoriler",
|
"Favorites": "Favoriler",
|
||||||
"Folders": "Klasörler",
|
"Folders": "Klasörler",
|
||||||
"Genres": "Türler",
|
"Genres": "Türler",
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
|
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
|
||||||
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
|
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
|
||||||
"HeaderFavoriteAlbums": "Album Ưa Thích",
|
"HeaderFavoriteAlbums": "Album Ưa Thích",
|
||||||
"FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập không thành công từ {0}",
|
"FailedLoginAttemptWithUserName": "Đăng nhập không thành công thử từ {0}",
|
||||||
"DeviceOnlineWithName": "{0} đã kết nối",
|
"DeviceOnlineWithName": "{0} đã kết nối",
|
||||||
"DeviceOfflineWithName": "{0} đã ngắt kết nối",
|
"DeviceOfflineWithName": "{0} đã ngắt kết nối",
|
||||||
"ChapterNameValue": "Phân Cảnh {0}",
|
"ChapterNameValue": "Phân Cảnh {0}",
|
||||||
@@ -127,7 +127,5 @@
|
|||||||
"TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay",
|
"TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay",
|
||||||
"TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật.",
|
"TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật.",
|
||||||
"TaskCleanCollectionsAndPlaylists": "Dọn dẹp bộ sưu tập và danh sách phát",
|
"TaskCleanCollectionsAndPlaylists": "Dọn dẹp bộ sưu tập và danh sách phát",
|
||||||
"TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại.",
|
"TaskCleanCollectionsAndPlaylistsDescription": "Xóa các mục khỏi bộ sưu tập và danh sách phát không còn tồn tại."
|
||||||
"TaskAudioNormalization": "Chuẩn Hóa Âm Thanh",
|
|
||||||
"TaskAudioNormalizationDescription": "Quét tập tin để tìm dữ liệu chuẩn hóa âm thanh."
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"Collections": "合集",
|
"Collections": "合集",
|
||||||
"DeviceOfflineWithName": "{0} 已断开",
|
"DeviceOfflineWithName": "{0} 已断开",
|
||||||
"DeviceOnlineWithName": "{0} 已连接",
|
"DeviceOnlineWithName": "{0} 已连接",
|
||||||
"FailedLoginAttemptWithUserName": "来自 {0} 的登录尝试失败",
|
"FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败",
|
||||||
"Favorites": "我的最爱",
|
"Favorites": "我的最爱",
|
||||||
"Folders": "文件夹",
|
"Folders": "文件夹",
|
||||||
"Genres": "类型",
|
"Genres": "类型",
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV
|
|||||||
}
|
}
|
||||||
|
|
||||||
string? presentationUniqueKey = null;
|
string? presentationUniqueKey = null;
|
||||||
int? limit = null;
|
int? limit = request.Limit;
|
||||||
if (!request.SeriesId.IsNullOrEmpty())
|
if (!request.SeriesId.IsNullOrEmpty())
|
||||||
{
|
{
|
||||||
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
|
if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
|
||||||
|
|||||||
@@ -180,7 +180,21 @@ public class LibraryStructureController : BaseJellyfinApiController
|
|||||||
// No need to start if scanning the library because it will handle it
|
// No need to start if scanning the library because it will handle it
|
||||||
if (refreshLibrary)
|
if (refreshLibrary)
|
||||||
{
|
{
|
||||||
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
await _libraryManager.ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
|
||||||
|
var newLib = _libraryManager.GetUserRootFolder().Children.FirstOrDefault(f => f.Path.Equals(newPath, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (newLib is CollectionFolder folder)
|
||||||
|
{
|
||||||
|
foreach (var child in folder.GetPhysicalFolders())
|
||||||
|
{
|
||||||
|
await child.RefreshMetadata(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
await child.ValidateChildren(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We don't know if this one can be validated individually, trigger a new validation
|
||||||
|
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Data</PackageId>
|
<PackageId>Jellyfin.Data</PackageId>
|
||||||
<VersionPrefix>10.10.0</VersionPrefix>
|
<VersionPrefix>10.9.6</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ public class TrickplayManager : ITrickplayManager
|
|||||||
var mediaPath = mediaSource.Path;
|
var mediaPath = mediaSource.Path;
|
||||||
if (!File.Exists(mediaPath))
|
if (!File.Exists(mediaPath))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Media not found at {Path} for item {ItemID}", mediaPath, video.Id);
|
_logger.LogWarning("Media source {MediaSourceId} not found at {Path} for item {ItemID}", mediaSource.Id, mediaPath, video.Id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.10.0</VersionPrefix>
|
<VersionPrefix>10.9.6</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
if (IsFileProtocol)
|
if (IsFileProtocol)
|
||||||
{
|
{
|
||||||
IEnumerable<BaseItem> nonCachedChildren;
|
IEnumerable<BaseItem> nonCachedChildren = [];
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -373,7 +373,6 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error retrieving children folder");
|
Logger.LogError(ex, "Error retrieving children folder");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.Report(ProgressHelpers.RetrievedChildren);
|
progress.Report(ProgressHelpers.RetrievedChildren);
|
||||||
|
|||||||
@@ -149,6 +149,14 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken);
|
Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reloads the root media folder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <param name="removeRoot">Is remove the library itself allowed.</param>
|
||||||
|
/// <returns>Task.</returns>
|
||||||
|
Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false);
|
||||||
|
|
||||||
Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false);
|
Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Controller</PackageId>
|
<PackageId>Jellyfin.Controller</PackageId>
|
||||||
<VersionPrefix>10.10.0</VersionPrefix>
|
<VersionPrefix>10.9.6</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
|
return _cache.GetOrAdd(path, static (p, fileSystem) => fileSystem.GetFileSystemEntries(p).ToArray(), _fileSystem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<FileSystemMetadata> GetDirectories(string path)
|
||||||
|
{
|
||||||
|
var list = new List<FileSystemMetadata>();
|
||||||
|
var items = GetFileSystemEntries(path);
|
||||||
|
for (var i = 0; i < items.Length; i++)
|
||||||
|
{
|
||||||
|
var item = items[i];
|
||||||
|
if (item.IsDirectory)
|
||||||
|
{
|
||||||
|
list.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
public List<FileSystemMetadata> GetFiles(string path)
|
public List<FileSystemMetadata> GetFiles(string path)
|
||||||
{
|
{
|
||||||
var list = new List<FileSystemMetadata>();
|
var list = new List<FileSystemMetadata>();
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
{
|
{
|
||||||
FileSystemMetadata[] GetFileSystemEntries(string path);
|
FileSystemMetadata[] GetFileSystemEntries(string path);
|
||||||
|
|
||||||
|
List<FileSystemMetadata> GetDirectories(string path);
|
||||||
|
|
||||||
List<FileSystemMetadata> GetFiles(string path);
|
List<FileSystemMetadata> GetFiles(string path);
|
||||||
|
|
||||||
FileSystemMetadata? GetFile(string path);
|
FileSystemMetadata? GetFile(string path);
|
||||||
|
|||||||
@@ -140,6 +140,14 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
|
IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
|
||||||
where T : BaseItem;
|
where T : BaseItem;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the metadata savers for the provided item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item.</param>
|
||||||
|
/// <param name="libraryOptions">The library options.</param>
|
||||||
|
/// <returns>The metadata savers.</returns>
|
||||||
|
IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all metadata plugins.
|
/// Gets all metadata plugins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -38,19 +38,28 @@ namespace MediaBrowser.LocalMetadata.Images
|
|||||||
}
|
}
|
||||||
|
|
||||||
var parentPathFiles = directoryService.GetFiles(parentPath);
|
var parentPathFiles = directoryService.GetFiles(parentPath);
|
||||||
|
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan()).ToString();
|
||||||
|
|
||||||
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan());
|
var thumbName = string.Concat(nameWithoutExtension, "-thumb");
|
||||||
|
var images = GetImageFilesFromFolder(thumbName, parentPathFiles);
|
||||||
|
|
||||||
return GetFilesFromParentFolder(nameWithoutExtension, parentPathFiles);
|
var metadataSubPath = directoryService.GetDirectories(parentPath).Where(d => d.Name.EndsWith("metadata", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
foreach (var path in metadataSubPath)
|
||||||
|
{
|
||||||
|
var files = directoryService.GetFiles(path.FullName);
|
||||||
|
images.AddRange(GetImageFilesFromFolder(nameWithoutExtension, files));
|
||||||
|
}
|
||||||
|
|
||||||
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<LocalImageInfo> GetFilesFromParentFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> parentPathFiles)
|
private List<LocalImageInfo> GetImageFilesFromFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> filePaths)
|
||||||
{
|
{
|
||||||
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
|
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
|
||||||
|
|
||||||
var list = new List<LocalImageInfo>(1);
|
var list = new List<LocalImageInfo>(1);
|
||||||
|
|
||||||
foreach (var i in parentPathFiles)
|
foreach (var i in filePaths)
|
||||||
{
|
{
|
||||||
if (i.IsDirectory)
|
if (i.IsDirectory)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -267,14 +267,14 @@ namespace MediaBrowser.Model.Entities
|
|||||||
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
|
attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
|
if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase) && !string.Equals(Codec, "dts", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
|
||||||
attributes.Add(Profile);
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(Codec))
|
|
||||||
{
|
{
|
||||||
attributes.Add(AudioCodec.GetFriendlyName(Codec));
|
attributes.Add(AudioCodec.GetFriendlyName(Codec));
|
||||||
}
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
attributes.Add(Profile);
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(ChannelLayout))
|
if (!string.IsNullOrEmpty(ChannelLayout))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Model</PackageId>
|
<PackageId>Jellyfin.Model</PackageId>
|
||||||
<VersionPrefix>10.10.0</VersionPrefix>
|
<VersionPrefix>10.9.6</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -33,10 +33,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<PackageReference Include="Microsoft.AspNetCore.HttpOverrides" />
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||||
<PackageReference Include="MimeTypes">
|
<PackageReference Include="MimeTypes">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
saveLocally = false;
|
saveLocally = false;
|
||||||
|
|
||||||
// If season is virtual under a physical series, save locally if using compatible convention
|
// If season is virtual under a physical series, save locally
|
||||||
if (item is Season season && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible)
|
if (item is Season season)
|
||||||
{
|
{
|
||||||
var series = season.Series;
|
var series = season.Series;
|
||||||
|
|
||||||
@@ -126,7 +126,11 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally);
|
var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally);
|
||||||
|
|
||||||
var retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false);
|
string[] retryPaths = [];
|
||||||
|
if (saveLocally)
|
||||||
|
{
|
||||||
|
retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false);
|
||||||
|
}
|
||||||
|
|
||||||
// If there are more than one output paths, the stream will need to be seekable
|
// If there are more than one output paths, the stream will need to be seekable
|
||||||
if (paths.Length > 1 && !source.CanSeek)
|
if (paths.Length > 1 && !source.CanSeek)
|
||||||
@@ -183,6 +187,13 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_fileSystem.DeleteFile(currentPath);
|
_fileSystem.DeleteFile(currentPath);
|
||||||
|
|
||||||
|
// Remove containing directory if empty
|
||||||
|
var folder = Path.GetDirectoryName(currentPath);
|
||||||
|
if (!_fileSystem.GetFiles(folder).Any())
|
||||||
|
{
|
||||||
|
Directory.Delete(folder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
@@ -374,6 +385,45 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to determine image file extension from mime type {0}", mimeType));
|
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to determine image file extension from mime type {0}", mimeType));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
extension = ".jpg";
|
||||||
|
}
|
||||||
|
|
||||||
|
extension = extension.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (type == ImageType.Primary && saveLocally)
|
||||||
|
{
|
||||||
|
if (season is not null && season.IndexNumber.HasValue)
|
||||||
|
{
|
||||||
|
var seriesFolder = season.SeriesPath;
|
||||||
|
|
||||||
|
var seasonMarker = season.IndexNumber.Value == 0
|
||||||
|
? "-specials"
|
||||||
|
: season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var imageFilename = "season" + seasonMarker + "-poster" + extension;
|
||||||
|
|
||||||
|
return Path.Combine(seriesFolder, imageFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == ImageType.Backdrop && saveLocally)
|
||||||
|
{
|
||||||
|
if (season is not null && season.IndexNumber.HasValue)
|
||||||
|
{
|
||||||
|
var seriesFolder = season.SeriesPath;
|
||||||
|
|
||||||
|
var seasonMarker = season.IndexNumber.Value == 0
|
||||||
|
? "-specials"
|
||||||
|
: season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var imageFilename = "season" + seasonMarker + "-fanart" + extension;
|
||||||
|
|
||||||
|
return Path.Combine(seriesFolder, imageFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (type == ImageType.Thumb && saveLocally)
|
if (type == ImageType.Thumb && saveLocally)
|
||||||
{
|
{
|
||||||
if (season is not null && season.IndexNumber.HasValue)
|
if (season is not null && season.IndexNumber.HasValue)
|
||||||
@@ -447,20 +497,12 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
extension = ".jpg";
|
|
||||||
}
|
|
||||||
|
|
||||||
extension = extension.ToLowerInvariant();
|
|
||||||
|
|
||||||
string path = null;
|
string path = null;
|
||||||
|
|
||||||
if (saveLocally)
|
if (saveLocally)
|
||||||
{
|
{
|
||||||
if (type == ImageType.Primary && item is Episode)
|
if (type == ImageType.Primary && item is Episode)
|
||||||
{
|
{
|
||||||
path = Path.Combine(Path.GetDirectoryName(item.Path), "metadata", filename + extension);
|
path = Path.Combine(Path.GetDirectoryName(item.Path), filename + "-thumb" + extension);
|
||||||
}
|
}
|
||||||
else if (item.IsInMixedFolder)
|
else if (item.IsInMixedFolder)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -371,12 +371,21 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
// nothing to do, already gone
|
// Nothing to do, already gone
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Unable to delete {Image}", image.Path);
|
_logger.LogWarning(ex, "Unable to delete {Image}", image.Path);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Always remove empty parent folder
|
||||||
|
var folder = Path.GetDirectoryName(image.Path);
|
||||||
|
if (Directory.Exists(folder) && !_fileSystem.GetFiles(folder).Any())
|
||||||
|
{
|
||||||
|
Directory.Delete(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +428,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
var type = _singularImages[i];
|
var type = _singularImages[i];
|
||||||
var image = GetFirstLocalImageInfoByType(images, type);
|
var image = GetFirstLocalImageInfoByType(images, type);
|
||||||
|
|
||||||
if (image is not null)
|
// Only use local images if we are not replacing and saving
|
||||||
|
if (image is not null && !(item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages))
|
||||||
{
|
{
|
||||||
var currentImage = item.GetImageInfo(type, 0);
|
var currentImage = item.GetImageInfo(type, 0);
|
||||||
// if image file is stored with media, don't replace that later
|
// if image file is stored with media, don't replace that later
|
||||||
|
|||||||
@@ -154,7 +154,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
id.IsAutomated = refreshOptions.IsAutomated;
|
id.IsAutomated = refreshOptions.IsAutomated;
|
||||||
|
|
||||||
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false);
|
var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any();
|
||||||
|
var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, hasMetadataSavers, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
updateType |= result.UpdateType;
|
updateType |= result.UpdateType;
|
||||||
if (result.Failures > 0)
|
if (result.Failures > 0)
|
||||||
@@ -639,6 +640,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
MetadataRefreshOptions options,
|
MetadataRefreshOptions options,
|
||||||
ICollection<IMetadataProvider> providers,
|
ICollection<IMetadataProvider> providers,
|
||||||
ItemImageProvider imageService,
|
ItemImageProvider imageService,
|
||||||
|
bool isSavingMetadata,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var refreshResult = new RefreshResult
|
var refreshResult = new RefreshResult
|
||||||
@@ -669,69 +671,74 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
temp.Item.Id = item.Id;
|
temp.Item.Id = item.Id;
|
||||||
|
|
||||||
var foundImageTypes = new List<ImageType>();
|
var foundImageTypes = new List<ImageType>();
|
||||||
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
|
|
||||||
|
// Do not execute local providers if we are identifying or replacing with local metadata saving enabled
|
||||||
|
if (options.SearchResult is null && !(isSavingMetadata && options.ReplaceAllMetadata))
|
||||||
{
|
{
|
||||||
var providerName = provider.GetType().Name;
|
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
|
||||||
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
|
||||||
|
|
||||||
var itemInfo = new ItemInfo(item);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false);
|
var providerName = provider.GetType().Name;
|
||||||
|
Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
|
||||||
|
|
||||||
if (localItem.HasMetadata)
|
var itemInfo = new ItemInfo(item);
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var remoteImage in localItem.RemoteImages)
|
var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (localItem.HasMetadata)
|
||||||
{
|
{
|
||||||
try
|
foreach (var remoteImage in localItem.RemoteImages)
|
||||||
{
|
{
|
||||||
if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
|
try
|
||||||
&& !options.IsReplacingImage(remoteImage.Type))
|
|
||||||
{
|
{
|
||||||
continue;
|
if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
|
||||||
|
&& !options.IsReplacingImage(remoteImage.Type))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
|
||||||
|
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||||
|
|
||||||
|
// remember imagetype that has just been downloaded
|
||||||
|
foundImageTypes.Add(remoteImage.Type);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url);
|
||||||
}
|
}
|
||||||
|
|
||||||
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
|
|
||||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
|
||||||
|
|
||||||
// remember imagetype that has just been downloaded
|
|
||||||
foundImageTypes.Add(remoteImage.Type);
|
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
|
||||||
|
if (foundImageTypes.Count > 0)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url);
|
imageService.UpdateReplaceImages(options, foundImageTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageService.MergeImages(item, localItem.Images, options))
|
||||||
|
{
|
||||||
|
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
|
||||||
|
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundImageTypes.Count > 0)
|
Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
|
||||||
{
|
|
||||||
imageService.UpdateReplaceImages(options, foundImageTypes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageService.MergeImages(item, localItem.Images, options))
|
|
||||||
{
|
|
||||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
|
|
||||||
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error in {Provider}", provider.Name);
|
||||||
|
|
||||||
Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
|
// If a local provider fails, consider that a failure
|
||||||
}
|
refreshResult.ErrorMessage = ex.Message;
|
||||||
catch (OperationCanceledException)
|
}
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error in {Provider}", provider.Name);
|
|
||||||
|
|
||||||
// If a local provider fails, consider that a failure
|
|
||||||
refreshResult.ErrorMessage = ex.Message;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -763,7 +770,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata;
|
var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata;
|
||||||
MergeData(temp, metadata, item.LockedFields, shouldReplace, false);
|
MergeData(temp, metadata, item.LockedFields, shouldReplace, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1050,7 +1057,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
target.Tags = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
|
target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1080,7 +1087,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).Distinct().ToArray();
|
target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).DistinctBy(t => t.Url).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
MergeAlbumArtist(source, target, replaceData);
|
MergeAlbumArtist(source, target, replaceData);
|
||||||
|
|||||||
@@ -418,6 +418,12 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false);
|
return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions)
|
||||||
|
{
|
||||||
|
return _savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, ItemUpdateType.MetadataEdit, false));
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata)
|
private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata)
|
||||||
where T : BaseItem
|
where T : BaseItem
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -193,11 +193,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
tags ??= new TagLib.Id3v2.Tag();
|
tags ??= new TagLib.Id3v2.Tag();
|
||||||
tags.AlbumArtists ??= mediaInfo.AlbumArtists;
|
tags.AlbumArtists = tags.AlbumArtists.Length == 0 ? mediaInfo.AlbumArtists : tags.AlbumArtists;
|
||||||
tags.Album ??= mediaInfo.Album;
|
tags.Album ??= mediaInfo.Album;
|
||||||
tags.Title ??= mediaInfo.Name;
|
tags.Title ??= mediaInfo.Name;
|
||||||
tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year;
|
tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year;
|
||||||
tags.Performers ??= mediaInfo.Artists;
|
tags.Performers = tags.Performers.Length == 0 ? mediaInfo.Artists : tags.Performers;
|
||||||
tags.Genres ??= mediaInfo.Genres;
|
tags.Genres ??= mediaInfo.Genres;
|
||||||
tags.Track = tags.Track == 0U ? Convert.ToUInt32(mediaInfo.IndexNumber, CultureInfo.InvariantCulture) : tags.Track;
|
tags.Track = tags.Track == 0U ? Convert.ToUInt32(mediaInfo.IndexNumber, CultureInfo.InvariantCulture) : tags.Track;
|
||||||
tags.Disc = tags.Disc == 0U ? Convert.ToUInt32(mediaInfo.ParentIndexNumber, CultureInfo.InvariantCulture) : tags.Disc;
|
tags.Disc = tags.Disc == 0U ? Convert.ToUInt32(mediaInfo.ParentIndexNumber, CultureInfo.InvariantCulture) : tags.Disc;
|
||||||
|
|||||||
@@ -119,7 +119,8 @@ namespace MediaBrowser.Providers.TV
|
|||||||
virtualSeason,
|
virtualSeason,
|
||||||
new DeleteOptions
|
new DeleteOptions
|
||||||
{
|
{
|
||||||
DeleteFileLocation = true
|
// Internal metadata paths are removed regardless of this.
|
||||||
|
DeleteFileLocation = false
|
||||||
},
|
},
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
@@ -176,7 +177,8 @@ namespace MediaBrowser.Providers.TV
|
|||||||
episode,
|
episode,
|
||||||
new DeleteOptions
|
new DeleteOptions
|
||||||
{
|
{
|
||||||
DeleteFileLocation = true
|
// Internal metadata paths are removed regardless of this.
|
||||||
|
DeleteFileLocation = false
|
||||||
},
|
},
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -825,7 +825,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
|||||||
private string GetOutputTrailerUrl(string url)
|
private string GetOutputTrailerUrl(string url)
|
||||||
{
|
{
|
||||||
// This is what xbmc expects
|
// This is what xbmc expects
|
||||||
return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/?action=play_video&videoid=", StringComparison.OrdinalIgnoreCase);
|
return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/play/?video_id=", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager)
|
private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager)
|
||||||
|
|||||||
@@ -45,27 +45,24 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
|||||||
|
|
||||||
internal static IEnumerable<string> GetMovieSavePaths(ItemInfo item)
|
internal static IEnumerable<string> GetMovieSavePaths(ItemInfo item)
|
||||||
{
|
{
|
||||||
|
var path = item.ContainingFolderPath;
|
||||||
if (item.VideoType == VideoType.Dvd && !item.IsPlaceHolder)
|
if (item.VideoType == VideoType.Dvd && !item.IsPlaceHolder)
|
||||||
{
|
{
|
||||||
var path = item.ContainingFolderPath;
|
|
||||||
|
|
||||||
yield return Path.Combine(path, "VIDEO_TS", "VIDEO_TS.nfo");
|
yield return Path.Combine(path, "VIDEO_TS", "VIDEO_TS.nfo");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
|
||||||
|
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
|
||||||
|
{
|
||||||
|
yield return Path.Combine(path, "movie.nfo");
|
||||||
|
}
|
||||||
|
|
||||||
if (!item.IsPlaceHolder && (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay))
|
if (!item.IsPlaceHolder && (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay))
|
||||||
{
|
{
|
||||||
var path = item.ContainingFolderPath;
|
|
||||||
|
|
||||||
yield return Path.Combine(path, Path.GetFileName(path) + ".nfo");
|
yield return Path.Combine(path, Path.GetFileName(path) + ".nfo");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
|
|
||||||
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
|
|
||||||
{
|
|
||||||
yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return Path.ChangeExtension(item.Path, ".nfo");
|
yield return Path.ChangeExtension(item.Path, ".nfo");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -153,20 +153,20 @@ API documentation can be viewed at `http://localhost:8096/api-docs/swagger/index
|
|||||||
|
|
||||||
As Jellyfin will run on a container on a github hosted server, JF needs to handle some things differently.
|
As Jellyfin will run on a container on a github hosted server, JF needs to handle some things differently.
|
||||||
|
|
||||||
**NOTE:** Depending on the selected configuration (if you just click 'create codespace' it will create a default configuration one) it might take 20-30 seconds to load all extensions and prepare the environment while VS Code is already open. Just give it some time and wait until you see `Downloading .NET version(s) 7.0.15~x64 ...... Done!` in the output tab.
|
**NOTE:** Depending on the selected configuration (if you just click 'create codespace' it will create a default configuration one) it might take 20-30 secounds to load all extensions and prepare the enviorment while vscode is already open. Just give it some time and wait until you see `Downloading .NET version(s) 7.0.15~x64 ...... Done!` in the output tab.
|
||||||
|
|
||||||
**NOTE:** If you want to access the JF instance from outside, like with a WebClient on another PC, remember to set the "ports" in the lower VS Code window to public.
|
**NOTE:** If you want to access the JF instance from outside, like with a WebClient on another PC, remember to set the "ports" in the lower VsCode window to public.
|
||||||
|
|
||||||
**NOTE:** When first opening the server instance with any WebUI, you will be sent to the login instead of the setup page. Refresh the login page once and you should be redirected to the Setup.
|
**NOTE:** When first opening the server instance with any WebUI, you will be send to the login instead of the setup page. Refresh the login page once and you should be redirected to the Setup.
|
||||||
|
|
||||||
There are two configurations for you to choose from.
|
There are two configurations for you to chose from.
|
||||||
#### Default - Development Jellyfin Server
|
#### Default - Development Jellyfin Server
|
||||||
This creates a container that has everything to run and debug the Jellyfin Media server but does not setup anything else. Each time you create a new container you have to run through the whole setup again. There is also no ffmpeg, webclient or media preloaded. Use the `.NET Launch (nowebclient)` launch config to start the server.
|
This creates a container that has everything to run and debug the Jellyfin Media server but does not setup anything else. Each time you create a new container you have to run though the whole setup again. There is also no ffmpeg, webclient or media preloaded. Use the `.NET Launch (nowebclient)` lunch config to start the server.
|
||||||
|
|
||||||
> Keep in mind that as this has no web client you have to connect to it via an external client. This can be just another codespace container running the WebUI. vuejs does not work from the get-go as it does not support the setup steps.
|
> Keep in mind that as this has no web client you have to connect to it via an extenal client. This can be just another codespace container running the WebUI. vuejs does not work from the getgo as it does not support the setup steps.
|
||||||
|
|
||||||
#### Development Jellyfin Server ffmpeg
|
#### Development Jellyfin Server ffmpeg
|
||||||
this extends the default server with a default installation of ffmpeg6 though the means described here: https://jellyfin.org/docs/general/installation/linux#repository-manual
|
this extens the default server with an default installation of ffmpeg6 though the means described here: https://jellyfin.org/docs/general/installation/linux#repository-manual
|
||||||
If you want to install a specific ffmpeg version, follow the comments embedded in the `.devcontainer/Dev - Server Ffmpeg/install.ffmpeg.sh` file.
|
If you want to install a specific ffmpeg version, follow the comments embedded in the `.devcontainer/Dev - Server Ffmpeg/install.ffmpeg.sh` file.
|
||||||
|
|
||||||
Use the `ghcs .NET Launch (nowebclient, ffmpeg)` launch config to run with the jellyfin-ffmpeg enabled.
|
Use the `ghcs .NET Launch (nowebclient, ffmpeg)` launch config to run with the jellyfin-ffmpeg enabled.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
[assembly: AssemblyVersion("10.10.0")]
|
[assembly: AssemblyVersion("10.9.6")]
|
||||||
[assembly: AssemblyFileVersion("10.10.0")]
|
[assembly: AssemblyFileVersion("10.9.6")]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Extensions</PackageId>
|
<PackageId>Jellyfin.Extensions</PackageId>
|
||||||
<VersionPrefix>10.10.0</VersionPrefix>
|
<VersionPrefix>10.9.6</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ namespace Jellyfin.Providers.Tests.Manager
|
|||||||
[InlineData(ImageType.Backdrop, 2, false)]
|
[InlineData(ImageType.Backdrop, 2, false)]
|
||||||
[InlineData(ImageType.Primary, 1, true)]
|
[InlineData(ImageType.Primary, 1, true)]
|
||||||
[InlineData(ImageType.Backdrop, 2, true)]
|
[InlineData(ImageType.Backdrop, 2, true)]
|
||||||
public async void RefreshImages_PopulatedItemPopulatedProviderDynamic_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
|
public async Task RefreshImages_PopulatedItemPopulatedProviderDynamic_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
|
||||||
{
|
{
|
||||||
var item = GetItemWithImages(imageType, imageCount, false);
|
var item = GetItemWithImages(imageType, imageCount, false);
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ namespace Jellyfin.Providers.Tests.Manager
|
|||||||
[InlineData(ImageType.Backdrop, 2, true, MediaProtocol.File)]
|
[InlineData(ImageType.Backdrop, 2, true, MediaProtocol.File)]
|
||||||
[InlineData(ImageType.Primary, 1, false, MediaProtocol.File)]
|
[InlineData(ImageType.Primary, 1, false, MediaProtocol.File)]
|
||||||
[InlineData(ImageType.Backdrop, 2, false, MediaProtocol.File)]
|
[InlineData(ImageType.Backdrop, 2, false, MediaProtocol.File)]
|
||||||
public async void RefreshImages_EmptyItemPopulatedProviderDynamic_AddsImages(ImageType imageType, int imageCount, bool responseHasPath, MediaProtocol protocol)
|
public async Task RefreshImages_EmptyItemPopulatedProviderDynamic_AddsImages(ImageType imageType, int imageCount, bool responseHasPath, MediaProtocol protocol)
|
||||||
{
|
{
|
||||||
// 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>();
|
||||||
@@ -311,7 +311,7 @@ namespace Jellyfin.Providers.Tests.Manager
|
|||||||
[InlineData(ImageType.Primary, 1, true)]
|
[InlineData(ImageType.Primary, 1, true)]
|
||||||
[InlineData(ImageType.Backdrop, 1, true)]
|
[InlineData(ImageType.Backdrop, 1, true)]
|
||||||
[InlineData(ImageType.Backdrop, 2, true)]
|
[InlineData(ImageType.Backdrop, 2, true)]
|
||||||
public async void RefreshImages_PopulatedItemPopulatedProviderRemote_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
|
public async Task RefreshImages_PopulatedItemPopulatedProviderRemote_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
|
||||||
{
|
{
|
||||||
var item = GetItemWithImages(imageType, imageCount, false);
|
var item = GetItemWithImages(imageType, imageCount, false);
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@ namespace Jellyfin.Providers.Tests.Manager
|
|||||||
[InlineData(ImageType.Backdrop, 0, false)] // empty item, no cache to check
|
[InlineData(ImageType.Backdrop, 0, false)] // empty item, no cache to check
|
||||||
[InlineData(ImageType.Backdrop, 1, false)] // populated item, cached so no download
|
[InlineData(ImageType.Backdrop, 1, false)] // populated item, cached so no download
|
||||||
[InlineData(ImageType.Backdrop, 1, true)] // populated item, forced to download
|
[InlineData(ImageType.Backdrop, 1, true)] // populated item, forced to download
|
||||||
public async void RefreshImages_NonStubItemPopulatedProviderRemote_DownloadsIfNecessary(ImageType imageType, int initialImageCount, bool fullRefresh)
|
public async Task RefreshImages_NonStubItemPopulatedProviderRemote_DownloadsIfNecessary(ImageType imageType, int initialImageCount, bool fullRefresh)
|
||||||
{
|
{
|
||||||
var targetImageCount = 1;
|
var targetImageCount = 1;
|
||||||
|
|
||||||
@@ -429,7 +429,7 @@ namespace Jellyfin.Providers.Tests.Manager
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(GetImageTypesWithCount))]
|
[MemberData(nameof(GetImageTypesWithCount))]
|
||||||
public async void RefreshImages_EmptyItemPopulatedProviderRemoteExtras_LimitsImages(ImageType imageType, int imageCount)
|
public async Task RefreshImages_EmptyItemPopulatedProviderRemoteExtras_LimitsImages(ImageType imageType, int imageCount)
|
||||||
{
|
{
|
||||||
var item = new Video();
|
var item = new Video();
|
||||||
|
|
||||||
@@ -473,7 +473,7 @@ namespace Jellyfin.Providers.Tests.Manager
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(GetImageTypesWithCount))]
|
[MemberData(nameof(GetImageTypesWithCount))]
|
||||||
public async void RefreshImages_PopulatedItemEmptyProviderRemoteFullRefresh_DoesntClearImages(ImageType imageType, int imageCount)
|
public async Task RefreshImages_PopulatedItemEmptyProviderRemoteFullRefresh_DoesntClearImages(ImageType imageType, int imageCount)
|
||||||
{
|
{
|
||||||
var item = GetItemWithImages(imageType, imageCount, false);
|
var item = GetItemWithImages(imageType, imageCount, false);
|
||||||
|
|
||||||
@@ -501,7 +501,7 @@ namespace Jellyfin.Providers.Tests.Manager
|
|||||||
[InlineData(9, false)]
|
[InlineData(9, false)]
|
||||||
[InlineData(10, true)]
|
[InlineData(10, true)]
|
||||||
[InlineData(null, true)]
|
[InlineData(null, true)]
|
||||||
public async void RefreshImages_ProviderRemote_FiltersByWidth(int? remoteImageWidth, bool expectedToUpdate)
|
public async Task RefreshImages_ProviderRemote_FiltersByWidth(int? remoteImageWidth, bool expectedToUpdate)
|
||||||
{
|
{
|
||||||
var imageType = ImageType.Primary;
|
var imageType = ImageType.Primary;
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public class AudioResolverTests
|
|||||||
[InlineData("My.Video.mp3", false, true)]
|
[InlineData("My.Video.mp3", false, true)]
|
||||||
[InlineData("My.Video.srt", true, false)]
|
[InlineData("My.Video.srt", true, false)]
|
||||||
[InlineData("My.Video.mp3", true, true)]
|
[InlineData("My.Video.mp3", true, true)]
|
||||||
public async void GetExternalStreams_MixedFilenames_PicksAudio(string file, bool metadataDirectory, bool matches)
|
public async Task GetExternalStreams_MixedFilenames_PicksAudio(string file, bool metadataDirectory, bool matches)
|
||||||
{
|
{
|
||||||
BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
|
BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void GetImage_NoStreams_ReturnsNoImage()
|
public async Task GetImage_NoStreams_ReturnsNoImage()
|
||||||
{
|
{
|
||||||
var input = new Movie();
|
var input = new Movie();
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo
|
|||||||
[InlineData("clearlogo.png", null, 1, ImageType.Logo, ImageFormat.Png)] // extract extension from name
|
[InlineData("clearlogo.png", null, 1, ImageType.Logo, ImageFormat.Png)] // extract extension from name
|
||||||
[InlineData("backdrop", "image/bmp", 2, ImageType.Backdrop, ImageFormat.Bmp)] // extract extension from mimetype
|
[InlineData("backdrop", "image/bmp", 2, ImageType.Backdrop, ImageFormat.Bmp)] // extract extension from mimetype
|
||||||
[InlineData("poster", null, 3, ImageType.Primary, ImageFormat.Jpg)] // default extension to jpg
|
[InlineData("poster", null, 3, ImageType.Primary, ImageFormat.Jpg)] // default extension to jpg
|
||||||
public async void GetImage_Attachment_ReturnsCorrectSelection(string filename, string? mimetype, int targetIndex, ImageType type, ImageFormat? expectedFormat)
|
public async Task GetImage_Attachment_ReturnsCorrectSelection(string filename, string? mimetype, int targetIndex, ImageType type, ImageFormat? expectedFormat)
|
||||||
{
|
{
|
||||||
var attachments = new List<MediaAttachment>();
|
var attachments = new List<MediaAttachment>();
|
||||||
string pathPrefix = "path";
|
string pathPrefix = "path";
|
||||||
@@ -103,7 +103,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo
|
|||||||
[InlineData(null, "mjpeg", 1, ImageType.Primary, ImageFormat.Jpg)]
|
[InlineData(null, "mjpeg", 1, ImageType.Primary, ImageFormat.Jpg)]
|
||||||
[InlineData(null, "png", 1, ImageType.Primary, ImageFormat.Png)]
|
[InlineData(null, "png", 1, ImageType.Primary, ImageFormat.Png)]
|
||||||
[InlineData(null, "webp", 1, ImageType.Primary, ImageFormat.Webp)]
|
[InlineData(null, "webp", 1, ImageType.Primary, ImageFormat.Webp)]
|
||||||
public async void GetImage_Embedded_ReturnsCorrectSelection(string? label, string? codec, int targetIndex, ImageType type, ImageFormat? expectedFormat)
|
public async Task GetImage_Embedded_ReturnsCorrectSelection(string? label, string? codec, int targetIndex, ImageType type, ImageFormat? expectedFormat)
|
||||||
{
|
{
|
||||||
var streams = new List<MediaStream>();
|
var streams = new List<MediaStream>();
|
||||||
for (int i = 1; i <= targetIndex; i++)
|
for (int i = 1; i <= targetIndex; i++)
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ public class MediaInfoResolverTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("https://url.com/My.Video.mkv")]
|
[InlineData("https://url.com/My.Video.mkv")]
|
||||||
[InlineData(VideoDirectoryPath)] // valid but no files found for this test
|
[InlineData(VideoDirectoryPath)] // valid but no files found for this test
|
||||||
public async void GetExternalStreams_BadPaths_ReturnsNoSubtitles(string path)
|
public async Task GetExternalStreams_BadPaths_ReturnsNoSubtitles(string path)
|
||||||
{
|
{
|
||||||
// need a media source manager capable of returning something other than file protocol
|
// need a media source manager capable of returning something other than file protocol
|
||||||
var mediaSourceManager = new Mock<IMediaSourceManager>();
|
var mediaSourceManager = new Mock<IMediaSourceManager>();
|
||||||
@@ -285,7 +285,7 @@ public class MediaInfoResolverTests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly_Data))]
|
[MemberData(nameof(GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly_Data))]
|
||||||
public async void GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly(string file, MediaStream[] inputStreams, MediaStream[] expectedStreams)
|
public async Task GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly(string file, MediaStream[] inputStreams, MediaStream[] expectedStreams)
|
||||||
{
|
{
|
||||||
BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
|
BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ public class MediaInfoResolverTests
|
|||||||
[InlineData(1, 2)]
|
[InlineData(1, 2)]
|
||||||
[InlineData(2, 1)]
|
[InlineData(2, 1)]
|
||||||
[InlineData(2, 2)]
|
[InlineData(2, 2)]
|
||||||
public async void GetExternalStreams_StreamIndex_HandlesFilesAndContainers(int fileCount, int streamCount)
|
public async Task GetExternalStreams_StreamIndex_HandlesFilesAndContainers(int fileCount, int streamCount)
|
||||||
{
|
{
|
||||||
BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
|
BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public class SubtitleResolverTests
|
|||||||
[InlineData("My.Video.mp3", false, false)]
|
[InlineData("My.Video.mp3", false, false)]
|
||||||
[InlineData("My.Video.srt", true, true)]
|
[InlineData("My.Video.srt", true, true)]
|
||||||
[InlineData("My.Video.mp3", true, false)]
|
[InlineData("My.Video.mp3", true, false)]
|
||||||
public async void GetExternalStreams_MixedFilenames_PicksSubtitles(string file, bool metadataDirectory, bool matches)
|
public async Task GetExternalStreams_MixedFilenames_PicksSubtitles(string file, bool metadataDirectory, bool matches)
|
||||||
{
|
{
|
||||||
BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
|
BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(GetImage_UnsupportedInput_ReturnsNoImage_TestData))]
|
[MemberData(nameof(GetImage_UnsupportedInput_ReturnsNoImage_TestData))]
|
||||||
public async void GetImage_UnsupportedInput_ReturnsNoImage(Video input)
|
public async Task GetImage_UnsupportedInput_ReturnsNoImage(Video input)
|
||||||
{
|
{
|
||||||
var mediaSourceManager = GetMediaSourceManager(input, null, new List<MediaStream>());
|
var mediaSourceManager = GetMediaSourceManager(input, null, new List<MediaStream>());
|
||||||
var videoImageProvider = new VideoImageProvider(mediaSourceManager, Mock.Of<IMediaEncoder>(), new NullLogger<VideoImageProvider>());
|
var videoImageProvider = new VideoImageProvider(mediaSourceManager, Mock.Of<IMediaEncoder>(), new NullLogger<VideoImageProvider>());
|
||||||
@@ -47,7 +47,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo
|
|||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(1, 1)] // default not first stream
|
[InlineData(1, 1)] // default not first stream
|
||||||
[InlineData(5, 0)] // default out of valid range
|
[InlineData(5, 0)] // default out of valid range
|
||||||
public async void GetImage_DefaultVideoStreams_ReturnsCorrectStreamImage(int defaultIndex, int targetIndex)
|
public async Task GetImage_DefaultVideoStreams_ReturnsCorrectStreamImage(int defaultIndex, int targetIndex)
|
||||||
{
|
{
|
||||||
var input = new Movie { DefaultVideoStreamIndex = defaultIndex };
|
var input = new Movie { DefaultVideoStreamIndex = defaultIndex };
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo
|
|||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(null, 10)] // default time
|
[InlineData(null, 10)] // default time
|
||||||
[InlineData(500, 50)] // calculated time
|
[InlineData(500, 50)] // calculated time
|
||||||
public async void GetImage_TimeSpan_SelectsCorrectTime(int? runTimeSeconds, long expectedSeconds)
|
public async Task GetImage_TimeSpan_SelectsCorrectTime(int? runTimeSeconds, long expectedSeconds)
|
||||||
{
|
{
|
||||||
MediaStream targetStream = new() { Type = MediaStreamType.Video, Index = 0 };
|
MediaStream targetStream = new() { Type = MediaStreamType.Video, Index = 0 };
|
||||||
var input = new Movie
|
var input = new Movie
|
||||||
|
|||||||
@@ -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