mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-02-12 07:32:25 +00:00
Compare commits
53 Commits
v10.11.0-r
...
v10.11.0-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f60281d8fd | ||
|
|
2936588c0f | ||
|
|
0e1be6ce30 | ||
|
|
4cd0a2ed8d | ||
|
|
aa05185917 | ||
|
|
2d9257b203 | ||
|
|
d1d9c8ed06 | ||
|
|
23c25289da | ||
|
|
aad6bca955 | ||
|
|
9f0f9a276f | ||
|
|
6016159860 | ||
|
|
6ffc044af1 | ||
|
|
c22f24319b | ||
|
|
1c4c9cf733 | ||
|
|
ea34a38f09 | ||
|
|
bbcfb2f421 | ||
|
|
0873fa8a86 | ||
|
|
9dc50b4ac6 | ||
|
|
617ab0d0ca | ||
|
|
dee9629037 | ||
|
|
31f3b5f6bb | ||
|
|
2ac6a7ba3f | ||
|
|
ece77779f8 | ||
|
|
c15c1f82a3 | ||
|
|
a15352b80c | ||
|
|
304b944152 | ||
|
|
e81c8ac6d1 | ||
|
|
97c1cb2f26 | ||
|
|
ac9d84f602 | ||
|
|
f3bf3c9853 | ||
|
|
644245bb7c | ||
|
|
a18c0007b4 | ||
|
|
c8a51160b4 | ||
|
|
4a0a45a045 | ||
|
|
91da1c035d | ||
|
|
6b5ce934b3 | ||
|
|
7174bb6a93 | ||
|
|
7037121bd0 | ||
|
|
7417da0e5c | ||
|
|
1e8bf1ce8d | ||
|
|
d4c3d24e52 | ||
|
|
d3ad2aec60 | ||
|
|
3554f068fb | ||
|
|
6dac1fde0a | ||
|
|
56fe4a158e | ||
|
|
c2332d340c | ||
|
|
d5b5c71baf | ||
|
|
7aee5b1e70 | ||
|
|
a8601b3797 | ||
|
|
1e9e4ffda9 | ||
|
|
d7faf9a327 | ||
|
|
bdb3adeb30 | ||
|
|
1f5cfb1e23 |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.5",
|
||||
"version": "9.0.6",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
6
.github/workflows/ci-codeql-analysis.yml
vendored
6
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -27,11 +27,11 @@ jobs:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
|
||||
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
|
||||
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
|
||||
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
- [cryptobank](https://github.com/cryptobank)
|
||||
- [cvium](https://github.com/cvium)
|
||||
- [dannymichel](https://github.com/dannymichel)
|
||||
- [darioackermann](https://github.com/darioackermann)
|
||||
- [DaveChild](https://github.com/DaveChild)
|
||||
- [DavidFair](https://github.com/DavidFair)
|
||||
- [Delgan](https://github.com/Delgan)
|
||||
@@ -196,6 +197,7 @@
|
||||
- [benedikt257](https://github.com/benedikt257)
|
||||
- [revam](https://github.com/revam)
|
||||
- [allesmi](https://github.com/allesmi)
|
||||
- [ThunderClapLP](https://github.com/ThunderClapLP)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -26,29 +26,29 @@
|
||||
<PackageVersion Include="libse" Version="4.0.12" />
|
||||
<PackageVersion Include="LrcParser" Version="2025.228.1" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.6" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
|
||||
@@ -59,7 +59,7 @@
|
||||
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.2.1" />
|
||||
<PackageVersion Include="Polly" Version="8.5.2" />
|
||||
<PackageVersion Include="Polly" Version="8.6.0" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
@@ -69,9 +69,10 @@
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
|
||||
<PackageVersion Include="SkiaSharp" Version="3.119.0" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.119.0" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.0" />
|
||||
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
|
||||
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg.Skia" Version="3.0.3" />
|
||||
@@ -79,11 +80,11 @@
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.6" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.6" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.6" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="6.24.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="6.26.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.2.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
|
||||
@@ -62,6 +62,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LibraryTaskScheduler;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Lyrics;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
@@ -552,6 +553,7 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
|
||||
serviceCollection.AddSingleton<ILimitedConcurrencyLibraryScheduler, LimitedConcurrencyLibraryScheduler>();
|
||||
|
||||
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
|
||||
|
||||
@@ -650,6 +652,7 @@ namespace Emby.Server.Implementations
|
||||
CollectionFolder.ApplicationHost = this;
|
||||
Folder.UserViewManager = Resolve<IUserViewManager>();
|
||||
Folder.CollectionManager = Resolve<ICollectionManager>();
|
||||
Folder.LimitedConcurrencyLibraryScheduler = Resolve<ILimitedConcurrencyLibraryScheduler>();
|
||||
Episode.MediaEncoder = Resolve<IMediaEncoder>();
|
||||
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
|
||||
Video.RecordingsManager = Resolve<IRecordingsManager>();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -6,6 +7,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.MediaSegments;
|
||||
using MediaBrowser.Controller.Trickplay;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library;
|
||||
|
||||
@@ -18,6 +20,7 @@ public class ExternalDataManager : IExternalDataManager
|
||||
private readonly IMediaSegmentManager _mediaSegmentManager;
|
||||
private readonly IPathManager _pathManager;
|
||||
private readonly ITrickplayManager _trickplayManager;
|
||||
private readonly ILogger<ExternalDataManager> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExternalDataManager"/> class.
|
||||
@@ -26,16 +29,19 @@ public class ExternalDataManager : IExternalDataManager
|
||||
/// <param name="mediaSegmentManager">The media segment manager.</param>
|
||||
/// <param name="pathManager">The path manager.</param>
|
||||
/// <param name="trickplayManager">The trickplay manager.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public ExternalDataManager(
|
||||
IKeyframeManager keyframeManager,
|
||||
IMediaSegmentManager mediaSegmentManager,
|
||||
IPathManager pathManager,
|
||||
ITrickplayManager trickplayManager)
|
||||
ITrickplayManager trickplayManager,
|
||||
ILogger<ExternalDataManager> logger)
|
||||
{
|
||||
_keyframeManager = keyframeManager;
|
||||
_mediaSegmentManager = mediaSegmentManager;
|
||||
_pathManager = pathManager;
|
||||
_trickplayManager = trickplayManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -47,7 +53,14 @@ public class ExternalDataManager : IExternalDataManager
|
||||
{
|
||||
foreach (var path in validPaths)
|
||||
{
|
||||
Directory.Delete(path, true);
|
||||
try
|
||||
{
|
||||
Directory.Delete(path, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Unable to prune external item data at {Path}: {Exception}", path, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2987,21 +2987,29 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (personEntity is null)
|
||||
{
|
||||
var path = Person.GetPath(person.Name);
|
||||
var info = Directory.CreateDirectory(path);
|
||||
var lastWriteTime = info.LastWriteTimeUtc;
|
||||
personEntity = new Person()
|
||||
try
|
||||
{
|
||||
Name = person.Name,
|
||||
Id = GetItemByNameId<Person>(path),
|
||||
DateCreated = info.CreationTimeUtc,
|
||||
DateModified = lastWriteTime,
|
||||
Path = path
|
||||
};
|
||||
var path = Person.GetPath(person.Name);
|
||||
var info = Directory.CreateDirectory(path);
|
||||
var lastWriteTime = info.LastWriteTimeUtc;
|
||||
personEntity = new Person()
|
||||
{
|
||||
Name = person.Name,
|
||||
Id = GetItemByNameId<Person>(path),
|
||||
DateCreated = info.CreationTimeUtc,
|
||||
DateModified = lastWriteTime,
|
||||
Path = path
|
||||
};
|
||||
|
||||
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
|
||||
saveEntity = true;
|
||||
createEntity = true;
|
||||
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
|
||||
saveEntity = true;
|
||||
createEntity = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to create person {Name}", person.Name);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var id in person.ProviderIds)
|
||||
|
||||
@@ -462,7 +462,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
{
|
||||
var movie = (T)result.Items[0];
|
||||
movie.IsInMixedFolder = false;
|
||||
movie.Name = Path.GetFileName(movie.ContainingFolderPath);
|
||||
if (collectionType == CollectionType.movies || collectionType is null)
|
||||
{
|
||||
movie.Name = Path.GetFileName(movie.ContainingFolderPath);
|
||||
}
|
||||
|
||||
return movie;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,6 @@ public class CollectionPostScanTask : ILibraryPostScanTask
|
||||
boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
|
||||
{
|
||||
Name = collectionName,
|
||||
IsLocked = true
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
"DeviceOnlineWithName": "{0} està connectat",
|
||||
"FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}",
|
||||
"Favorites": "Preferits",
|
||||
"Folders": "Carpetes",
|
||||
"Folders": "Directoris",
|
||||
"Genres": "Gèneres",
|
||||
"HeaderAlbumArtists": "Artistes de l'àlbum",
|
||||
"HeaderContinueWatching": "Continua veient",
|
||||
"HeaderContinueWatching": "Continueu mirant",
|
||||
"HeaderFavoriteAlbums": "Àlbums preferits",
|
||||
"HeaderFavoriteArtists": "Artistes preferits",
|
||||
"HeaderFavoriteEpisodes": "Episodis preferits",
|
||||
@@ -24,11 +24,11 @@
|
||||
"HeaderFavoriteSongs": "Cançons preferides",
|
||||
"HeaderLiveTV": "TV en directe",
|
||||
"HeaderNextUp": "A continuació",
|
||||
"HeaderRecordingGroups": "Grups Musicals",
|
||||
"HeaderRecordingGroups": "Grups musicals",
|
||||
"HomeVideos": "Vídeos domèstics",
|
||||
"Inherit": "Heretat",
|
||||
"ItemAddedWithName": "{0} s'ha afegit a la biblioteca",
|
||||
"ItemRemovedWithName": "{0} s'ha eliminat de la biblioteca",
|
||||
"ItemAddedWithName": "{0} s'ha afegit a la mediateca",
|
||||
"ItemRemovedWithName": "{0} s'ha eliminat de la mediateca",
|
||||
"LabelIpAddressValue": "Adreça IP: {0}",
|
||||
"LabelRunningTimeValue": "Temps en marxa: {0}",
|
||||
"Latest": "Darrers",
|
||||
@@ -72,10 +72,10 @@
|
||||
"ServerNameNeedsToBeRestarted": "S'ha de reiniciar {0}",
|
||||
"Shows": "Sèries",
|
||||
"Songs": "Cançons",
|
||||
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu de nou en una estona.",
|
||||
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
|
||||
"Sync": "Sincronitzar",
|
||||
"Sync": "Sincronitza",
|
||||
"System": "Sistema",
|
||||
"TvShows": "Sèries de TV",
|
||||
"User": "Usuari",
|
||||
@@ -89,7 +89,7 @@
|
||||
"UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1} a {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1} a {2}",
|
||||
"ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la teva biblioteca",
|
||||
"ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la mediateca",
|
||||
"ValueSpecialEpisodeName": "Especial - {0}",
|
||||
"VersionNumber": "Versió {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
|
||||
@@ -98,43 +98,45 @@
|
||||
"TaskRefreshChannels": "Actualitza els canals",
|
||||
"TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
|
||||
"TaskCleanTranscode": "Neteja les transcodificacions",
|
||||
"TaskUpdatePluginsDescription": "Actualitza els complements que estan configurats per a actualitzar-se automàticament.",
|
||||
"TaskUpdatePluginsDescription": "Descarrega i instal·la els complements que estiguin configurats per a actualitzar-se automàticament.",
|
||||
"TaskUpdatePlugins": "Actualitza els complements",
|
||||
"TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva biblioteca de mitjans.",
|
||||
"TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la mediateca.",
|
||||
"TaskRefreshPeople": "Actualitza les persones",
|
||||
"TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
|
||||
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
|
||||
"TaskCleanLogs": "Neteja els registres",
|
||||
"TaskRefreshLibraryDescription": "Escaneja la biblioteca de mitjans buscant fitxers nous i refresca les metadades.",
|
||||
"TaskRefreshLibrary": "Escaneja la biblioteca de mitjans",
|
||||
"TaskRefreshLibraryDescription": "Escaneja la mediateca, a la cerca de fitxers nous i refresca les metadades.",
|
||||
"TaskRefreshLibrary": "Escaneja la mediateca",
|
||||
"TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.",
|
||||
"TaskRefreshChapterImages": "Extreure les imatges dels capítols",
|
||||
"TaskRefreshChapterImages": "Extreu les imatges dels capítols",
|
||||
"TaskCleanCacheDescription": "Elimina la memòria cau no necessària per al servidor.",
|
||||
"TaskCleanCache": "Elimina la memòria cau",
|
||||
"TasksChannelsCategory": "Canals per internet",
|
||||
"TasksApplicationCategory": "Aplicatiu",
|
||||
"TasksLibraryCategory": "Biblioteca",
|
||||
"TasksLibraryCategory": "Mediateca",
|
||||
"TasksMaintenanceCategory": "Manteniment",
|
||||
"TaskCleanActivityLogDescription": "Eliminades les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
|
||||
"TaskCleanActivityLog": "Buidar el registre d'activitat",
|
||||
"TaskCleanActivityLog": "Buida el registre d'activitat",
|
||||
"Undefined": "Indefinit",
|
||||
"Forced": "Forçat",
|
||||
"Default": "Per defecte",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
|
||||
"TaskOptimizeDatabase": "Optimitzar la base de dades",
|
||||
"TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.",
|
||||
"TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la mediateca o fer d'altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
|
||||
"TaskOptimizeDatabase": "Optimitza la base de dades",
|
||||
"TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.",
|
||||
"TaskKeyframeExtractor": "Extractor de fotogrames clau",
|
||||
"External": "Extern",
|
||||
"HearingImpaired": "Discapacitat auditiva",
|
||||
"TaskRefreshTrickplayImages": "Generar miniatures de línia de temps",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.",
|
||||
"TaskRefreshTrickplayImages": "Genera imatges de previsualització",
|
||||
"TaskRefreshTrickplayImagesDescription": "Crea imatges de previsualització per vídeos en les mediateques habilitades.",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Neteja les col·leccions i llistes de reproducció",
|
||||
"TaskAudioNormalization": "Estabilització d'Àudio",
|
||||
"TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització d'àudio.",
|
||||
"TaskDownloadMissingLyricsDescription": "Baixar les lletres de les cançons",
|
||||
"TaskDownloadMissingLyrics": "Baixar les lletres que falten",
|
||||
"TaskAudioNormalization": "Estabilització de l'àudio",
|
||||
"TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització de l'àudio.",
|
||||
"TaskDownloadMissingLyricsDescription": "Baixa les lletres de les cançons",
|
||||
"TaskDownloadMissingLyrics": "Baixa les lletres que falten",
|
||||
"TaskExtractMediaSegments": "Escaneig de segments multimèdia",
|
||||
"TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.",
|
||||
"TaskMoveTrickplayImages": "Migra la ubicació de la imatge de Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Mou els fitxers trickplay existents segons la configuració de la biblioteca."
|
||||
"TaskMoveTrickplayImagesDescription": "Mou els fitxers trickplay existents segons la configuració de la mediateca.",
|
||||
"CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.",
|
||||
"CleanupUserDataTask": "Tasca de neteja de dades d'usuari"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Skenování segmentů médií",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrahuje či získá segmenty médií pomocí zásuvných modulů MediaSegment.",
|
||||
"TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny."
|
||||
"TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny.",
|
||||
"CleanupUserDataTaskDescription": "Odstraní všechna uživatelská data (stav zhlédnutí, oblíbené atd.) z médií, které již neexistují více než 90 dní.",
|
||||
"CleanupUserDataTask": "Pročistit uživatelská data"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Mediensegmente scannen",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrahiert oder empfängt Mediensegmente von Plugins die Mediensegmente nutzen.",
|
||||
"TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren",
|
||||
"TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben."
|
||||
"TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben.",
|
||||
"CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten",
|
||||
"CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Anschaustatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskExtractMediaSegments": "Media Segment Scan",
|
||||
"TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
|
||||
"TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
|
||||
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings."
|
||||
"TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.",
|
||||
"CleanupUserDataTask": "User data cleanup task",
|
||||
"CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favorite status etc) from media that is no longer present for at least 90 days."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
|
||||
"TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes",
|
||||
"TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment."
|
||||
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
|
||||
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
|
||||
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Analyse des segments de média",
|
||||
"TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
|
||||
"TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque."
|
||||
"TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
|
||||
"CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
|
||||
"CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskMoveTrickplayImages": "Trickplay attēlu pārvietošana",
|
||||
"TaskMoveTrickplayImagesDescription": "Pārvieto esošos trickplay failus atbilstoši bibliotēkas iestatījumiem.",
|
||||
"TaskDownloadMissingLyrics": "Lejupielādēt trūkstošos vārdus",
|
||||
"TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām"
|
||||
"TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām",
|
||||
"CleanupUserDataTask": "Lietotāju datu tīrīšanas uzdevums",
|
||||
"CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas."
|
||||
}
|
||||
|
||||
@@ -1,14 +1,141 @@
|
||||
{
|
||||
"Books": "Номууд",
|
||||
"HeaderNextUp": "Дараах",
|
||||
"HeaderNextUp": "Дараа нь",
|
||||
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
|
||||
"Songs": "Дуунууд",
|
||||
"Playlists": "Тоглуулах жагсаалт",
|
||||
"Movies": "Кино",
|
||||
"Latest": "Сүүлийн үеийн",
|
||||
"Genres": "Төрөл зүйл",
|
||||
"Genres": "Төрлүүд",
|
||||
"Favorites": "Дуртай",
|
||||
"Collections": "Багц",
|
||||
"Artists": "Зураачуд",
|
||||
"Albums": "Цомгууд"
|
||||
"Artists": "Уран бүтээлчид",
|
||||
"Albums": "Цомгууд",
|
||||
"TaskExtractMediaSegments": "Медиа сегмент шалга",
|
||||
"TaskExtractMediaSegmentsDescription": "MediaSegment идэвхжүүлсэн залгаасуудаас медиа сегментүүдийг задлах эсвэл олж авах.",
|
||||
"TaskMoveTrickplayImages": "Трикплэй зургуудын байршлыг шилжүүлэх",
|
||||
"TaskMoveTrickplayImagesDescription": "Одоогоор байгаа трикплэй файлуудыг сангийн тохиргоонд тохируулан шилжүүлнэ.",
|
||||
"TaskDownloadMissingLyrics": "Алга болсон дууны үгийг татаж авах",
|
||||
"TaskDownloadMissingLyricsDescription": "Дууны үгийг татаж авах",
|
||||
"TaskOptimizeDatabase": "Датабаазыг сайжруулах",
|
||||
"TaskKeyframeExtractor": "Түлхүүр кадр гаргагч",
|
||||
"TaskCleanCache": "Кэш санг цэвэрлэх",
|
||||
"NewVersionIsAvailable": "Jellyfin Server-н шинэ хувилбар татаж авахад нээлттэй боллоо.",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server-н {0}-р хэсгийн тохиргоо шинэчлэгдлээ",
|
||||
"NotificationOptionAudioPlaybackStopped": "Дууг зогсоов",
|
||||
"NotificationOptionNewLibraryContent": "Шинэ агуулга орлоо",
|
||||
"NotificationOptionServerRestartRequired": "Server-г дахин асаана уу",
|
||||
"NotificationOptionVideoPlaybackStopped": "Бичлэгийг зогсоов",
|
||||
"UserPasswordChangedWithName": "Хэрэглэгч {0}-н нууц үгийг өөрчиллөө",
|
||||
"TaskCleanCollectionsAndPlaylists": "Цуглуулга ба тоглуулах жагсаалтыг цэвэрлэх",
|
||||
"ScheduledTaskFailedWithName": "{0} амжилтгүй",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server ачааллаж байна. Хэсэг хугацааны дараа дахин оролдоно уу.",
|
||||
"TaskCleanActivityLog": "Үйл ажиллагааны бүртгэлийг цэвэрлэх",
|
||||
"SubtitleDownloadFailureFromForItem": "{0}-г {1}-д зориулсан хадмал орчуулгыг татаж авч чадсангүй",
|
||||
"TaskRefreshLibraryDescription": "Таны медиа санг шинэ файлуудын хувьд шалгаж, мета мэдээллийг шинэчилнэ.",
|
||||
"UserOfflineFromDevice": "{0}-г {1}-с салгалаа",
|
||||
"ValueHasBeenAddedToLibrary": "{0}-г медиа сан руу нэмэгдлээ",
|
||||
"TaskRefreshPeopleDescription": "Таны медиа санд байгаа жүжигчид болон найруулагчдын мета мэдээллийг шинэчилнэ.",
|
||||
"TaskCleanTranscodeDescription": "Нэг өдрөөс илүү настай транскодлох файлуудыг устгана.",
|
||||
"TaskRefreshChannelsDescription": "Интернет сувгуудын мэдээллийг шинэчлэх.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Мета мэдээллийн тохиргоонд үндэслэн интернетээс алга болсон дэд гарчгийг хайна.",
|
||||
"TaskOptimizeDatabaseDescription": "Мэдээллийн сантайг шахаж, чөлөөтэй зайг багасгана. Санг шалгаж, мэдээллийн сантай холбоотой өөрчлөлт хийхийн дараа энэ үйлдлийг гүйцэтгэх нь гүйцэтгэлийг сайжруулах боломжтой.",
|
||||
"TaskKeyframeExtractorDescription": "Видео файлуудаас түлхүүр кадруудыг гаргаж, илүү нарийвчилсан HLS тоглуулах жагсаалт үүсгэнэ. Энэ үйлдэл удаан хугацаанд үргэлжлэх боломжтой.",
|
||||
"NotificationOptionAudioPlayback": "Дууг тоглууллаа",
|
||||
"TaskRefreshTrickplayImages": "Трикплэй зургуудыг үүсгэх",
|
||||
"TaskUpdatePlugins": "Plugin-уудыг шинэчлэх",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Одоо байхгүй болсон зүйлсийг цуглуулга ба тоглуулах жагсаалтаас устгана.",
|
||||
"TaskAudioNormalization": "Аудиог хэвшүүлэх",
|
||||
"TaskAudioNormalizationDescription": "Файлуудаас дууны хэвийн хэмжээсийн мэдээллийг шалгана.",
|
||||
"TaskRefreshTrickplayImagesDescription": "Идэвхжсэн сангуудад байгаа видеонуудын трикплэй урьдчилсан харагдацыг үүсгэнэ.",
|
||||
"TaskUpdatePluginsDescription": "Автомат шинэчлэлд тохируулсан залгаасуудын шинэчлэлтийг татаж авч суулгана.",
|
||||
"TaskCleanTranscode": "Транскодлох санг цэвэрлэх",
|
||||
"TaskRefreshChannels": "Сувгуудыг шинэчлэх",
|
||||
"TaskDownloadMissingSubtitles": "Алга болсон хадмал орчуулгыг татах",
|
||||
"External": "Гадны",
|
||||
"HeaderFavoriteArtists": "Дуртай уран бүтээлчид",
|
||||
"HeaderFavoriteEpisodes": "Дуртай ангиуд",
|
||||
"HeaderFavoriteShows": "Дуртай нэвтрүүлэг",
|
||||
"HeaderFavoriteSongs": "Дуртай дуу",
|
||||
"AppDeviceValues": "Aпп: {0}, Төхөөрөмж: {1}",
|
||||
"Application": "Aпп",
|
||||
"AuthenticationSucceededWithUserName": "{0} амжилттай нэвтэрлээ",
|
||||
"CameraImageUploadedFrom": "{0}-с шинэ зураг байршуулагдлаа",
|
||||
"Channels": "Сувгууд",
|
||||
"ChapterNameValue": "{0}-р бүлэг",
|
||||
"Default": "Өгөгдмөл",
|
||||
"DeviceOfflineWithName": "{0}-н холболт саллаа",
|
||||
"DeviceOnlineWithName": "{0} холбогдлоо",
|
||||
"FailedLoginAttemptWithUserName": "{0}-н нэвтрэх оролдлого амжилтгүй",
|
||||
"Folders": "Хавтаснууд",
|
||||
"Forced": "Хүчээр",
|
||||
"HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
|
||||
"HeaderFavoriteAlbums": "Дуртай цомгууд",
|
||||
"HeaderLiveTV": "Шууд",
|
||||
"HeaderRecordingGroups": "Бичлэгийн бүлгүүд",
|
||||
"HearingImpaired": "Сонсголын бэрхшээлтэй",
|
||||
"HomeVideos": "Үндсэн дүрсүүд",
|
||||
"Inherit": "Уламжлах",
|
||||
"ItemAddedWithName": "{0}-г санд нэмлээ",
|
||||
"ItemRemovedWithName": "{0}-с сангаас хаслаа",
|
||||
"LabelIpAddressValue": "IP хаяг: {0}",
|
||||
"LabelRunningTimeValue": "Үргэлжлэх хугацаа: {0}",
|
||||
"MessageApplicationUpdated": "Jellyfin Server шинэчлэгдлээ",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server {0} болж шинэчлэгдлээ",
|
||||
"MessageServerConfigurationUpdated": "Server-н тохиргоо шинэчлэгдлээ",
|
||||
"MixedContent": "Холимог агуулга",
|
||||
"Music": "Дуу",
|
||||
"MusicVideos": "Дууны клип",
|
||||
"NameInstallFailed": "{0} суулгахад алдаа гарлаа",
|
||||
"NameSeasonNumber": "{0}-р улирал",
|
||||
"NameSeasonUnknown": "Улирал олдсонгүй",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Апп шинэчлэлт бий болсон байна",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Апп-н шинэчлэлийг суулгалаа",
|
||||
"NotificationOptionCameraImageUploaded": "Камерын зураг орууллаа",
|
||||
"NotificationOptionInstallationFailed": "Суулгалт амжилтгүй",
|
||||
"NotificationOptionPluginError": "Plugin-д алдаа гарлаа",
|
||||
"NotificationOptionPluginInstalled": "Plugin-г суулгалаа",
|
||||
"NotificationOptionPluginUninstalled": "Plugin-г устгалаа",
|
||||
"NotificationOptionPluginUpdateInstalled": "Plugin-ны шинэчлэн суулгалаа",
|
||||
"NotificationOptionTaskFailed": "Товолсон ажил амжилтгүй",
|
||||
"NotificationOptionUserLockedOut": "Хэрэглэгчийг түгжив",
|
||||
"NotificationOptionVideoPlayback": "Бичлэгийг тоглуулж эхлэв",
|
||||
"Photos": "Зургууд",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0}-г суулгалаа",
|
||||
"PluginUninstalledWithName": "{0}-г устгалаа",
|
||||
"PluginUpdatedWithName": "{0}-г шинэчиллээ",
|
||||
"ProviderValue": "Нийлүүлэгч: {0}",
|
||||
"ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
|
||||
"ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
|
||||
"Shows": "Нэвтрүүлгүүд",
|
||||
"Sync": "Дахин",
|
||||
"System": "Систем",
|
||||
"TvShows": "ТВ нэвтрүүлгүүд",
|
||||
"Undefined": "Танисангүй",
|
||||
"User": "Хэрэглэгч",
|
||||
"UserCreatedWithName": "Хэрэглэгч {0}-г үүсгэлээ",
|
||||
"UserDeletedWithName": "Хэрэглэгч {0}-г устгалаа",
|
||||
"UserDownloadingItemWithValues": "{0} нь {1}-г татаж байна",
|
||||
"UserLockedOutWithName": "Хэрэглэгч {0}-г түгжлээ",
|
||||
"UserOnlineFromDevice": "{0} нь {1}-тэй холбоотой байна",
|
||||
"UserPolicyUpdatedWithName": "Хэрэглэгчийн журмыг {0}-д зориулан шинэчиллээ",
|
||||
"UserStartedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж байна",
|
||||
"UserStoppedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж дуусгалаа",
|
||||
"ValueSpecialEpisodeName": "Тусгай - {0}",
|
||||
"VersionNumber": "Хувилбар {0}",
|
||||
"TasksMaintenanceCategory": "Засвар",
|
||||
"TasksLibraryCategory": "Сан",
|
||||
"TasksApplicationCategory": "Апп",
|
||||
"TasksChannelsCategory": "Интернет сувгууд",
|
||||
"TaskCleanActivityLogDescription": "Тохируулсан хугацаанаас хуучин үйл ажиллагааны бүртгэлийн бичлэгүүдийг устгана.",
|
||||
"TaskCleanLogs": "Бүртгэлийн санг цэвэрлэх",
|
||||
"TaskCleanLogsDescription": "{0} өдрөөс илүү настай бүртгэлийн файлуудыг устгана.",
|
||||
"TaskRefreshPeople": "Хүмүүсийг шинэчлэх",
|
||||
"TaskCleanCacheDescription": "Системд хэрэггүй болсон кэш файлуудыг устгана.",
|
||||
"TaskRefreshChapterImages": "Бүлгийн зураг авах",
|
||||
"TaskRefreshChapterImagesDescription": "Бүлгүүдтэй видеонуудын хуудсан зураг үүсгэнэ.",
|
||||
"TaskRefreshLibrary": "Медиа санг шалгах",
|
||||
"CleanupUserDataTask": "Хэрэглэгчийн өгөгдлийн цэвэрлэгээний үүрэг",
|
||||
"CleanupUserDataTaskDescription": "Хугацаа нь 90 хоногоос дээш хугацаанд байхгүй болсон медианаас бүх хэрэглэгчийн өгөгдлийг (үзсэн төлөв, дуртай жагсаалт гэх мэт) цэвэрлэнэ."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Verkrijgt mediasegmenten vanuit plug-ins met MediaSegment-ondersteuning.",
|
||||
"TaskMoveTrickplayImages": "Locatie trickplay-afbeeldingen migreren",
|
||||
"TaskMoveTrickplayImagesDescription": "Verplaatst bestaande trickplay-bestanden op basis van de bibliotheekinstellingen.",
|
||||
"TaskExtractMediaSegments": "Scannen op mediasegmenten"
|
||||
"TaskExtractMediaSegments": "Scannen op mediasegmenten",
|
||||
"CleanupUserDataTaskDescription": "Wist alle gebruikersgegevens (kijkstatus, favorieten, etc.) van media die al minstens 90 dagen niet meer aanwezig is.",
|
||||
"CleanupUserDataTask": "Opruimtaak gebruikersdata"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Skanowanie segmentów mediów",
|
||||
"TaskMoveTrickplayImages": "Migruj lokalizację obrazu Trickplay",
|
||||
"TaskExtractMediaSegmentsDescription": "Wyodrębnia lub pobiera segmenty mediów z wtyczek obsługujących MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki."
|
||||
"TaskMoveTrickplayImagesDescription": "Przenosi istniejące pliki Trickplay zgodnie z ustawieniami biblioteki.",
|
||||
"CleanupUserDataTaskDescription": "Usuwa wszystkie dane użytkownika (stan oglądanych, status ulubionych itp.) z mediów, które nie są dostępne od co najmniej 90 dni.",
|
||||
"CleanupUserDataTask": "Zadanie czyszczenia danych użytkownika"
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "Перенесение местоположения изображений Trickplay",
|
||||
"TaskExtractMediaSegments": "Сканирование медиасегментов",
|
||||
"TaskExtractMediaSegmentsDescription": "Извлекает или получает медиасегменты из плагинов MediaSegment.",
|
||||
"TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки."
|
||||
"TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки.",
|
||||
"CleanupUserDataTask": "Задача очистки пользовательских данных",
|
||||
"CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с носителей, на которых больше нет информации, по крайней мере, в течение 90 дней."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskExtractMediaSegments": "Skanning av mediesegment",
|
||||
"TaskExtractMediaSegmentsDescription": "Extraherar eller hämtar ut mediesegmen från tillägg som stöder MediaSegment.",
|
||||
"TaskMoveTrickplayImages": "Migrera platsen för Trickplay-bilder",
|
||||
"TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar."
|
||||
"TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar.",
|
||||
"CleanupUserDataTaskDescription": "Tar bort all användardata (såsom vad du sett, favoriter med mera) för media som inte funnits på enheten på minst 90 dagar.",
|
||||
"CleanupUserDataTask": "Uppgift för rensning av användardata"
|
||||
}
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
"DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว",
|
||||
"DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว",
|
||||
"Collections": "คอลเลกชัน",
|
||||
"ChapterNameValue": "บท {0}",
|
||||
"ChapterNameValue": "บทที่ {0}",
|
||||
"Channels": "ช่อง",
|
||||
"CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}",
|
||||
"Books": "หนังสือ",
|
||||
"AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว",
|
||||
"AuthenticationSucceededWithUserName": "{0} ยืนยันตัวตนสำเร็จแล้ว",
|
||||
"Artists": "ศิลปิน",
|
||||
"Application": "แอปพลิเคชัน",
|
||||
"AppDeviceValues": "แอป: {0}, อุปกรณ์: {1}",
|
||||
@@ -132,5 +132,8 @@
|
||||
"TaskAudioNormalizationDescription": "สแกนไฟล์เพื่อค้นหาข้อมูลการปรับระดับเสียงให้สม่ำเสมอ",
|
||||
"TaskCleanCollectionsAndPlaylists": "จัดระเบียบคอลเลกชันและเพลย์ลิสต์",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "ลบรายการออกจากคอลเลกชันและเพลย์ลิสต์ที่ไม่มีแล้ว",
|
||||
"TaskExtractMediaSegments": "การสแกนส่วนของสื่อมีเดีย"
|
||||
"TaskExtractMediaSegments": "การสแกนส่วนของสื่อมีเดีย",
|
||||
"TaskMoveTrickplayImagesDescription": "ย้ายไฟล์ Trickplay ตามการตั้งค่าของไลบรารี",
|
||||
"TaskExtractMediaSegmentsDescription": "แยกหรือดึงส่วนของสื่อจากปลั๊กอินที่เปิดใช้งาน MediaSegment",
|
||||
"TaskMoveTrickplayImages": "ย้ายตำแหน่งเก็บภาพตัวอย่าง Trickplay"
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskMoveTrickplayImagesDescription": "Переміщує наявні Trickplay-зображення відповідно до налаштувань медіатеки.",
|
||||
"TaskExtractMediaSegments": "Сканування медіа-сегментів",
|
||||
"TaskMoveTrickplayImages": "Змінити місце розташування Trickplay-зображень",
|
||||
"TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment."
|
||||
"TaskExtractMediaSegmentsDescription": "Витягує або отримує медіа-сегменти з плагінів з підтримкою MediaSegment.",
|
||||
"CleanupUserDataTask": "Завдання очищення даних користувача",
|
||||
"CleanupUserDataTaskDescription": "Очищає всі дані користувача (стан перегляду, статус обраного тощо) з медіа, які перестали бути доступними щонайменше 90 днів тому."
|
||||
}
|
||||
|
||||
@@ -135,5 +135,7 @@
|
||||
"TaskExtractMediaSegmentsDescription": "Trích xuất hoặc lấy các phân đoạn phương tiện từ các plugin hỗ trợ MediaSegment.",
|
||||
"TaskMoveTrickplayImages": "Di chuyển vị trí hình ảnh Trickplay",
|
||||
"TaskMoveTrickplayImagesDescription": "Di chuyển các tập tin trickplay hiện có theo cài đặt thư viện.",
|
||||
"TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện"
|
||||
"TaskExtractMediaSegments": "Quét Phân Đoạn Phương Tiện",
|
||||
"CleanupUserDataTask": "Tác vụ dọn dẹp dữ liệu người dùng",
|
||||
"CleanupUserDataTaskDescription": "Làm sạch tất cả dữ liệu người dùng (trạng thái xem, trạng thái yêu thích, v.v.) từ phương tiện không còn có mặt trong ít nhất 90 ngày."
|
||||
}
|
||||
|
||||
@@ -136,5 +136,7 @@
|
||||
"TaskMoveTrickplayImages": "迁移进度条预览图的存储位置",
|
||||
"TaskExtractMediaSegments": "媒体分段扫描",
|
||||
"TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体分段。",
|
||||
"TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。"
|
||||
"TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。",
|
||||
"CleanupUserDataTask": "用户数据清理任务",
|
||||
"CleanupUserDataTaskDescription": "清理已被删除超过90天的媒体中的所有用户数据(观看状态、收藏夹状态等)。"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.Implementations.Item;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Task to clean up any detached userdata from the database.
|
||||
/// </summary>
|
||||
public class CleanupUserDataTask : IScheduledTask
|
||||
{
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
private readonly ILogger<CleanupUserDataTask> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CleanupUserDataTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localization">The localisation Provider.</param>
|
||||
/// <param name="dbProvider">The DB context factory.</param>
|
||||
/// <param name="logger">A logger.</param>
|
||||
public CleanupUserDataTask(ILocalizationManager localization, IDbContextFactory<JellyfinDbContext> dbProvider, ILogger<CleanupUserDataTask> logger)
|
||||
{
|
||||
_localization = localization;
|
||||
_dbProvider = dbProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("CleanupUserDataTask");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("CleanupUserDataTaskDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => nameof(CleanupUserDataTask);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
const int LimitDays = 90;
|
||||
var userDataDate = DateTime.UtcNow.AddDays(LimitDays * -1);
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var detachedUserData = dbContext.UserData.Where(e => e.ItemId == BaseItemRepository.PlaceholderId);
|
||||
_logger.LogInformation("There are {NoDetached} detached UserData entries.", detachedUserData.Count());
|
||||
|
||||
detachedUserData = detachedUserData.Where(e => e.RetentionDate < userDataDate);
|
||||
|
||||
_logger.LogInformation("{NoDetached} are older then {Limit} days.", detachedUserData.Count(), LimitDays);
|
||||
|
||||
await detachedUserData.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
@@ -54,12 +54,12 @@ public class RefreshMediaLibraryTask : IScheduledTask
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
progress.Report(0);
|
||||
|
||||
return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken);
|
||||
await ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +456,7 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
var nowPlayingQueue = info.NowPlayingQueue;
|
||||
|
||||
if (nowPlayingQueue?.Length > 0)
|
||||
if (nowPlayingQueue?.Length > 0 && !nowPlayingQueue.SequenceEqual(session.NowPlayingQueue))
|
||||
{
|
||||
session.NowPlayingQueue = nowPlayingQueue;
|
||||
|
||||
|
||||
@@ -85,7 +85,10 @@ public class SystemManager : ISystemManager
|
||||
/// <inheritdoc/>
|
||||
public SystemStorageInfo GetSystemStorageInfo()
|
||||
{
|
||||
var virtualFolderInfos = _libraryManager.GetVirtualFolders().Select(e => new LibraryStorageInfo()
|
||||
var virtualFolderInfos = _libraryManager
|
||||
.GetVirtualFolders()
|
||||
.Where(e => !string.IsNullOrWhiteSpace(e.ItemId)) // this should not be null but for some users it is.
|
||||
.Select(e => new LibraryStorageInfo()
|
||||
{
|
||||
Id = Guid.Parse(e.ItemId),
|
||||
Name = e.Name,
|
||||
|
||||
@@ -292,12 +292,12 @@ public class BackupService : IBackupService
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
var migrations = await historyRepository.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
|
||||
ICollection<(Type Type, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes = [
|
||||
ICollection<(Type Type, string SourceName, Func<IAsyncEnumerable<object>> ValueFactory)> entityTypes = [
|
||||
.. typeof(JellyfinDbContext)
|
||||
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
||||
.Where(e => e.PropertyType.IsAssignableTo(typeof(IQueryable)))
|
||||
.Select(e => (Type: e.PropertyType, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!, e.PropertyType)))),
|
||||
(Type: typeof(HistoryRow), ValueFactory: new Func<IAsyncEnumerable<object>>(() => migrations.ToAsyncEnumerable()))
|
||||
.Select(e => (Type: e.PropertyType, dbContext.Model.FindEntityType(e.PropertyType.GetGenericArguments()[0])!.GetSchemaQualifiedTableName()!, ValueFactory: new Func<IAsyncEnumerable<object>>(() => GetValues((IQueryable)e.GetValue(dbContext)!, e.PropertyType)))),
|
||||
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: new Func<IAsyncEnumerable<object>>(() => migrations.ToAsyncEnumerable()))
|
||||
];
|
||||
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
|
||||
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
|
||||
@@ -308,8 +308,8 @@ public class BackupService : IBackupService
|
||||
|
||||
foreach (var entityType in entityTypes)
|
||||
{
|
||||
_logger.LogInformation("Begin backup of entity {Table}", entityType.Type.Name);
|
||||
var zipEntry = zipArchive.CreateEntry($"Database\\{entityType.Type.Name}.json");
|
||||
_logger.LogInformation("Begin backup of entity {Table}", entityType.SourceName);
|
||||
var zipEntry = zipArchive.CreateEntry($"Database\\{entityType.SourceName}.json");
|
||||
var entities = 0;
|
||||
var zipEntryStream = zipEntry.Open();
|
||||
await using (zipEntryStream.ConfigureAwait(false))
|
||||
|
||||
@@ -14,6 +14,7 @@ using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
@@ -53,6 +54,11 @@ namespace Jellyfin.Server.Implementations.Item;
|
||||
public sealed class BaseItemRepository
|
||||
: IItemRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the placeholder id for UserData detached items.
|
||||
/// </summary>
|
||||
public static readonly Guid PlaceholderId = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||
|
||||
/// <summary>
|
||||
/// This holds all the types in the running assemblies
|
||||
/// so that we can de-serialize properly when we don't have strong types.
|
||||
@@ -95,13 +101,21 @@ public sealed class BaseItemRepository
|
||||
/// <inheritdoc />
|
||||
public void DeleteItem(Guid id)
|
||||
{
|
||||
if (id.IsEmpty())
|
||||
if (id.IsEmpty() || id.Equals(PlaceholderId))
|
||||
{
|
||||
throw new ArgumentException("Guid can't be empty", nameof(id));
|
||||
throw new ArgumentException("Guid can't be empty or the placeholder id.", nameof(id));
|
||||
}
|
||||
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
using var transaction = context.Database.BeginTransaction();
|
||||
|
||||
var date = (DateTime?)DateTime.UtcNow;
|
||||
// Detach all user watch data
|
||||
context.UserData.Where(e => e.ItemId == id)
|
||||
.ExecuteUpdate(e => e
|
||||
.SetProperty(f => f.RetentionDate, date)
|
||||
.SetProperty(f => f.ItemId, PlaceholderId));
|
||||
|
||||
context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete();
|
||||
context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete();
|
||||
@@ -144,7 +158,7 @@ public sealed class BaseItemRepository
|
||||
PrepareFilterQuery(filter);
|
||||
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
return ApplyQueryFilter(context.BaseItems.AsNoTracking(), context, filter).Select(e => e.Id).ToArray();
|
||||
return ApplyQueryFilter(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, filter).Select(e => e.Id).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -319,7 +333,7 @@ public sealed class BaseItemRepository
|
||||
.Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value))
|
||||
.Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode])
|
||||
.Join(
|
||||
context.UserData.AsNoTracking(),
|
||||
context.UserData.AsNoTracking().Where(e => e.ItemId != EF.Constant(PlaceholderId)),
|
||||
i => new { UserId = filter.User.Id, ItemId = i.Id },
|
||||
u => new { UserId = u.UserId, ItemId = u.ItemId },
|
||||
(entity, data) => new { Item = entity, UserData = data })
|
||||
@@ -472,7 +486,7 @@ public sealed class BaseItemRepository
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var tuples = new List<(BaseItemDto Item, List<Guid>? AncestorIds, BaseItemDto TopParent, IEnumerable<string> UserDataKey, List<string> InheritedTags)>();
|
||||
foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()))
|
||||
foreach (var item in items.GroupBy(e => e.Id).Select(e => e.Last()).Where(e => e.Id != PlaceholderId))
|
||||
{
|
||||
var ancestorIds = item.SupportsAncestors ?
|
||||
item.GetAncestorIds().Distinct().ToList() :
|
||||
@@ -491,6 +505,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
var ids = tuples.Select(f => f.Item.Id).ToArray();
|
||||
var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
|
||||
var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
|
||||
|
||||
foreach (var item in tuples)
|
||||
{
|
||||
@@ -511,6 +526,19 @@ public sealed class BaseItemRepository
|
||||
|
||||
context.SaveChanges();
|
||||
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
// reattach old userData entries
|
||||
var userKeys = item.UserDataKey.ToArray();
|
||||
var retentionDate = (DateTime?)null;
|
||||
context.UserData
|
||||
.Where(e => e.ItemId == PlaceholderId)
|
||||
.Where(e => userKeys.Contains(e.CustomDataKey))
|
||||
.ExecuteUpdate(e => e
|
||||
.SetProperty(f => f.ItemId, item.Item.Id)
|
||||
.SetProperty(f => f.RetentionDate, retentionDate));
|
||||
}
|
||||
|
||||
var itemValueMaps = tuples
|
||||
.Select(e => (Item: e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
|
||||
.ToArray();
|
||||
@@ -1049,7 +1077,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
using var context = _dbProvider.CreateDbContext();
|
||||
|
||||
var innerQueryFilter = TranslateQuery(context.BaseItems, context, new InternalItemsQuery(filter.User)
|
||||
var innerQueryFilter = TranslateQuery(context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)), context, new InternalItemsQuery(filter.User)
|
||||
{
|
||||
ExcludeItemTypes = filter.ExcludeItemTypes,
|
||||
IncludeItemTypes = filter.IncludeItemTypes,
|
||||
@@ -1138,7 +1166,7 @@ public sealed class BaseItemRepository
|
||||
IsPlayed = filter.IsPlayed
|
||||
};
|
||||
|
||||
itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking(), context, typeSubQuery)
|
||||
itemCountQuery = TranslateQuery(context.BaseItems.AsNoTracking().Where(e => e.Id != EF.Constant(PlaceholderId)), context, typeSubQuery)
|
||||
.Where(e => e.ItemValues!.Any(f => itemValueTypes!.Contains(f.ItemValue.Type)));
|
||||
|
||||
var seriesTypeName = _itemTypeLookup.BaseItemKindNames[BaseItemKind.Series];
|
||||
@@ -1814,7 +1842,7 @@ public sealed class BaseItemRepository
|
||||
// We should probably figure this out for all folders, but for right now, this is the only place where we need it
|
||||
if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => context.BaseItems
|
||||
baseQuery = baseQuery.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
|
||||
.Where(e => e.IsFolder == false && e.IsVirtualItem == false)
|
||||
.Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
|
||||
.Any(f => f.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
|
||||
@@ -2064,7 +2092,7 @@ public sealed class BaseItemRepository
|
||||
if (filter.HasDeadParentId.HasValue && filter.HasDeadParentId.Value)
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e => e.ParentId.HasValue && !context.BaseItems.Any(f => f.Id == e.ParentId.Value));
|
||||
.Where(e => e.ParentId.HasValue && !context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Any(f => f.Id == e.ParentId.Value));
|
||||
}
|
||||
|
||||
if (filter.IsDeadArtist.HasValue && filter.IsDeadArtist.Value)
|
||||
@@ -2145,17 +2173,19 @@ public sealed class BaseItemRepository
|
||||
if (filter.ExcludeItemIds.Length > 0)
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e => !filter.ItemIds.Contains(e.Id));
|
||||
.Where(e => !filter.ExcludeItemIds.Contains(e.Id));
|
||||
}
|
||||
|
||||
if (filter.ExcludeProviderIds is not null && filter.ExcludeProviderIds.Count > 0)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => !e.Provider!.All(f => !filter.ExcludeProviderIds.All(w => f.ProviderId == w.Key && f.ProviderValue == w.Value)));
|
||||
var exclude = filter.ExcludeProviderIds.Select(e => $"{e.Key}:{e.Value}").ToArray();
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.All(f => !exclude.Contains(f)));
|
||||
}
|
||||
|
||||
if (filter.HasAnyProviderId is not null && filter.HasAnyProviderId.Count > 0)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Any(f => !filter.HasAnyProviderId.Any(w => f.ProviderId == w.Key && f.ProviderValue == w.Value)));
|
||||
var include = filter.HasAnyProviderId.Select(e => $"{e.Key}:{e.Value}").ToArray();
|
||||
baseQuery = baseQuery.Where(e => e.Provider!.Select(f => f.ProviderId + ":" + f.ProviderValue)!.Any(f => include.Contains(f)));
|
||||
}
|
||||
|
||||
if (filter.HasImdbId.HasValue)
|
||||
@@ -2197,7 +2227,7 @@ public sealed class BaseItemRepository
|
||||
if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey))
|
||||
{
|
||||
baseQuery = baseQuery
|
||||
.Where(e => context.BaseItems.Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.Children!.Any(w => w.ItemId == e.Id)));
|
||||
.Where(e => context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId)).Where(f => f.PresentationUniqueKey == filter.AncestorWithPresentationUniqueKey).Any(f => f.Children!.Any(w => w.ItemId == e.Id)));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
|
||||
@@ -2324,4 +2354,14 @@ public sealed class BaseItemRepository
|
||||
|
||||
return baseQuery;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<bool> ItemExistsAsync(Guid id)
|
||||
{
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
return await dbContext.BaseItems.AnyAsync(f => f.Id == id).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class MediaSegmentManager : IMediaSegmentManager
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool overwrite, CancellationToken cancellationToken)
|
||||
public async Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool forceOverwrite, CancellationToken cancellationToken)
|
||||
{
|
||||
var providers = _segmentProviders
|
||||
.Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name)))
|
||||
@@ -70,18 +70,13 @@ public class MediaSegmentManager : IMediaSegmentManager
|
||||
|
||||
using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!overwrite && (await db.MediaSegments.AnyAsync(e => e.ItemId.Equals(baseItem.Id), cancellationToken).ConfigureAwait(false)))
|
||||
{
|
||||
_logger.LogDebug("Skip {MediaPath} as it already contains media segments", baseItem.Path);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Start media segment extraction for {MediaPath} with {CountProviders} providers enabled", baseItem.Path, providers.Count);
|
||||
|
||||
await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// no need to recreate the request object every time.
|
||||
var requestItem = new MediaSegmentGenerationRequest() { ItemId = baseItem.Id };
|
||||
if (forceOverwrite)
|
||||
{
|
||||
// delete all existing media segments if forceOverwrite is set.
|
||||
await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var provider in providers)
|
||||
{
|
||||
@@ -91,15 +86,56 @@ public class MediaSegmentManager : IMediaSegmentManager
|
||||
continue;
|
||||
}
|
||||
|
||||
IQueryable<MediaSegment> existingSegments;
|
||||
if (forceOverwrite)
|
||||
{
|
||||
existingSegments = Array.Empty<MediaSegment>().AsQueryable();
|
||||
}
|
||||
else
|
||||
{
|
||||
existingSegments = db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id) && e.SegmentProviderId == GetProviderId(provider.Name));
|
||||
}
|
||||
|
||||
var requestItem = new MediaSegmentGenerationRequest()
|
||||
{
|
||||
ItemId = baseItem.Id,
|
||||
ExistingSegments = existingSegments.Select(e => Map(e)).ToArray()
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var segments = await provider.GetMediaSegments(requestItem, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (segments.Count == 0)
|
||||
|
||||
if (!forceOverwrite)
|
||||
{
|
||||
var existingSegmentsList = existingSegments.ToArray(); // Cannot use requestItem's list, as the provider might tamper with its items.
|
||||
if (segments.Count == requestItem.ExistingSegments.Count && segments.All(e => existingSegmentsList.Any(f =>
|
||||
{
|
||||
return
|
||||
e.StartTicks == f.StartTicks &&
|
||||
e.EndTicks == f.EndTicks &&
|
||||
e.Type == f.Type;
|
||||
})))
|
||||
{
|
||||
_logger.LogDebug("Media Segment provider {ProviderName} did not modify any segments for {MediaPath}", provider.Name, baseItem.Path);
|
||||
continue;
|
||||
}
|
||||
|
||||
// delete existing media segments that were re-generated.
|
||||
await existingSegments.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (segments.Count == 0 && !requestItem.ExistingSegments.Any())
|
||||
{
|
||||
_logger.LogDebug("Media Segment provider {ProviderName} did not find any segments for {MediaPath}", provider.Name, baseItem.Path);
|
||||
continue;
|
||||
}
|
||||
else if (segments.Count == 0 && requestItem.ExistingSegments.Any())
|
||||
{
|
||||
_logger.LogDebug("Media Segment provider {ProviderName} deleted all segments for {MediaPath}", provider.Name, baseItem.Path);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path);
|
||||
var providerId = GetProviderId(provider.Name);
|
||||
|
||||
@@ -116,26 +116,7 @@ namespace Jellyfin.Server.Extensions
|
||||
.AddTransient<ICorsPolicyProvider, CorsPolicyProvider>()
|
||||
.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
// https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
|
||||
// Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
|
||||
|
||||
if (config.KnownProxies.Length == 0)
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.None;
|
||||
options.KnownNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
|
||||
AddProxyAddresses(config, config.KnownProxies, options);
|
||||
}
|
||||
|
||||
// Only set forward limit if we have some known proxies or some known networks.
|
||||
if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0)
|
||||
{
|
||||
options.ForwardLimit = null;
|
||||
}
|
||||
ConfigureForwardHeaders(config, options);
|
||||
})
|
||||
.AddMvc(opts =>
|
||||
{
|
||||
@@ -183,6 +164,30 @@ namespace Jellyfin.Server.Extensions
|
||||
return mvcBuilder.AddControllersAsServices();
|
||||
}
|
||||
|
||||
internal static void ConfigureForwardHeaders(NetworkConfiguration config, ForwardedHeadersOptions options)
|
||||
{
|
||||
// https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
|
||||
// Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
|
||||
|
||||
if (config.KnownProxies.Length == 0)
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.None;
|
||||
options.KnownNetworks.Clear();
|
||||
options.KnownProxies.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
|
||||
AddProxyAddresses(config, config.KnownProxies, options);
|
||||
}
|
||||
|
||||
// Only set forward limit if we have some known proxies or some known networks.
|
||||
if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0)
|
||||
{
|
||||
options.ForwardLimit = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds Swagger to the service collection.
|
||||
/// </summary>
|
||||
|
||||
@@ -47,7 +47,7 @@ internal class JellyfinMigrationService
|
||||
public JellyfinMigrationService(
|
||||
IDbContextFactory<JellyfinDbContext> dbContextFactory,
|
||||
ILoggerFactory loggerFactory,
|
||||
IStartupLogger startupLogger,
|
||||
IStartupLogger<JellyfinMigrationService> startupLogger,
|
||||
IApplicationPaths applicationPaths,
|
||||
IBackupService? backupService = null,
|
||||
IJellyfinDatabaseProvider? jellyfinDatabaseProvider = null)
|
||||
|
||||
@@ -35,7 +35,7 @@ public class MigrateKeyframeData : IDatabaseMigrationRoutine
|
||||
/// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||
/// <param name="dbProvider">The EFCore db factory.</param>
|
||||
public MigrateKeyframeData(
|
||||
IStartupLogger startupLogger,
|
||||
IStartupLogger<MigrateKeyframeData> startupLogger,
|
||||
IApplicationPaths appPaths,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider)
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
/// <param name="paths">The server application paths.</param>
|
||||
/// <param name="jellyfinDatabaseProvider">The database provider for special access.</param>
|
||||
public MigrateLibraryDb(
|
||||
IStartupLogger startupLogger,
|
||||
IStartupLogger<MigrateLibraryDb> startupLogger,
|
||||
IDbContextFactory<JellyfinDbContext> provider,
|
||||
IServerApplicationPaths paths,
|
||||
IJellyfinDatabaseProvider jellyfinDatabaseProvider)
|
||||
|
||||
@@ -26,7 +26,7 @@ public class MigrateLibraryDbCompatibilityCheck : IAsyncMigrationRoutine
|
||||
/// </summary>
|
||||
/// <param name="startupLogger">The startup logger.</param>
|
||||
/// <param name="paths">The Path service.</param>
|
||||
public MigrateLibraryDbCompatibilityCheck(IStartupLogger startupLogger, IServerApplicationPaths paths)
|
||||
public MigrateLibraryDbCompatibilityCheck(IStartupLogger<MigrateLibraryDbCompatibilityCheck> startupLogger, IServerApplicationPaths paths)
|
||||
{
|
||||
_logger = startupLogger;
|
||||
_paths = paths;
|
||||
|
||||
@@ -23,7 +23,7 @@ internal class MigrateRatingLevels : IDatabaseMigrationRoutine
|
||||
|
||||
public MigrateRatingLevels(
|
||||
IDbContextFactory<JellyfinDbContext> provider,
|
||||
IStartupLogger logger,
|
||||
IStartupLogger<MigrateRatingLevels> logger,
|
||||
ILocalizationManager localizationManager)
|
||||
{
|
||||
_provider = provider;
|
||||
|
||||
@@ -47,7 +47,7 @@ public class MoveExtractedFiles : IAsyncMigrationRoutine
|
||||
public MoveExtractedFiles(
|
||||
IApplicationPaths appPaths,
|
||||
ILogger<MoveExtractedFiles> logger,
|
||||
IStartupLogger startupLogger,
|
||||
IStartupLogger<MoveExtractedFiles> startupLogger,
|
||||
IPathManager pathManager,
|
||||
IFileSystem fileSystem,
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider)
|
||||
|
||||
@@ -37,7 +37,7 @@ public class MoveTrickplayFiles : IMigrationRoutine
|
||||
ITrickplayManager trickplayManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IStartupLogger logger)
|
||||
IStartupLogger<MoveTrickplayFiles> logger)
|
||||
{
|
||||
_trickplayManager = trickplayManager;
|
||||
_fileSystem = fileSystem;
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using Jellyfin.Server.ServerSetupApp;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.Migrations.Stages;
|
||||
|
||||
@@ -21,11 +22,13 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta
|
||||
return Metadata.Order.ToString("yyyyMMddHHmmsss", CultureInfo.InvariantCulture) + "_" + Metadata.Name!;
|
||||
}
|
||||
|
||||
private ServiceCollection MigrationServices(IServiceProvider serviceProvider, IStartupLogger logger)
|
||||
private IServiceCollection MigrationServices(IServiceProvider serviceProvider, IStartupLogger logger)
|
||||
{
|
||||
var childServiceCollection = new ServiceCollection();
|
||||
childServiceCollection.AddSingleton(serviceProvider);
|
||||
childServiceCollection.AddSingleton(logger);
|
||||
var childServiceCollection = new ServiceCollection()
|
||||
.AddSingleton(serviceProvider)
|
||||
.AddSingleton(logger)
|
||||
.AddSingleton(typeof(IStartupLogger<>), typeof(NestedStartupLogger<>))
|
||||
.AddSingleton<StartupLogTopic>(logger.Topic!);
|
||||
|
||||
foreach (ServiceDescriptor service in serviceProvider.GetRequiredService<IServiceCollection>())
|
||||
{
|
||||
@@ -78,4 +81,11 @@ internal class CodeMigration(Type migrationType, JellyfinMigrationAttribute meta
|
||||
throw new InvalidOperationException($"The type {MigrationType} does not implement either IMigrationRoutine or IAsyncMigrationRoutine and is not a valid migration type");
|
||||
}
|
||||
}
|
||||
|
||||
private class NestedStartupLogger<TCategory> : StartupLogger<TCategory>, IStartupLogger<TCategory>
|
||||
{
|
||||
public NestedStartupLogger(ILogger logger, StartupLogTopic topic) : base(logger, topic)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Jellyfin.Server
|
||||
private static long _startTimestamp;
|
||||
private static ILogger _logger = NullLogger.Instance;
|
||||
private static bool _restartOnShutdown;
|
||||
private static IStartupLogger? _migrationLogger;
|
||||
private static IStartupLogger<JellyfinMigrationService>? _migrationLogger;
|
||||
private static string? _restoreFromBackup;
|
||||
|
||||
/// <summary>
|
||||
@@ -103,6 +103,7 @@ namespace Jellyfin.Server
|
||||
_setupServer = new SetupServer(static () => _jellyfinHost?.Services?.GetService<INetworkManager>(), appPaths, static () => _appHost, _loggerFactory, startupConfig);
|
||||
await _setupServer.RunAsync().ConfigureAwait(false);
|
||||
_logger = _loggerFactory.CreateLogger("Main");
|
||||
StartupLogger.Logger = new StartupLogger(_logger);
|
||||
|
||||
// Use the logging framework for uncaught exceptions instead of std error
|
||||
AppDomain.CurrentDomain.UnhandledException += (_, e)
|
||||
@@ -178,7 +179,9 @@ namespace Jellyfin.Server
|
||||
})
|
||||
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
|
||||
.UseSerilog()
|
||||
.ConfigureServices(e => e.AddTransient<IStartupLogger, StartupLogger>().AddSingleton<IServiceCollection>(e))
|
||||
.ConfigureServices(e => e
|
||||
.RegisterStartupLogger()
|
||||
.AddSingleton<IServiceCollection>(e))
|
||||
.Build();
|
||||
|
||||
// Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
|
||||
@@ -268,7 +271,7 @@ namespace Jellyfin.Server
|
||||
/// <returns>A task.</returns>
|
||||
public static async Task ApplyStartupMigrationAsync(ServerApplicationPaths appPaths, IConfiguration startupConfig)
|
||||
{
|
||||
_migrationLogger = StartupLogger.Logger.BeginGroup($"Migration Service");
|
||||
_migrationLogger = StartupLogger.Logger.BeginGroup<JellyfinMigrationService>($"Migration Service");
|
||||
var startupConfigurationManager = new ServerConfigurationManager(appPaths, _loggerFactory, new MyXmlSerializer());
|
||||
startupConfigurationManager.AddParts([new DatabaseConfigurationFactory()]);
|
||||
var migrationStartupServiceProvider = new ServiceCollection()
|
||||
@@ -276,7 +279,7 @@ namespace Jellyfin.Server
|
||||
.AddJellyfinDbContext(startupConfigurationManager, startupConfig)
|
||||
.AddSingleton<IApplicationPaths>(appPaths)
|
||||
.AddSingleton<ServerApplicationPaths>(appPaths)
|
||||
.AddSingleton<IStartupLogger>(_migrationLogger);
|
||||
.RegisterStartupLogger();
|
||||
|
||||
migrationStartupServiceProvider.AddSingleton(migrationStartupServiceProvider);
|
||||
var startupService = migrationStartupServiceProvider.BuildServiceProvider();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Morestachio.Helper.Logging;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace Jellyfin.Server.ServerSetupApp;
|
||||
@@ -9,6 +8,11 @@ namespace Jellyfin.Server.ServerSetupApp;
|
||||
/// </summary>
|
||||
public interface IStartupLogger : ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the topic this logger is assigned to.
|
||||
/// </summary>
|
||||
StartupLogTopic? Topic { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds another logger instance to this logger for combined logging.
|
||||
/// </summary>
|
||||
@@ -22,4 +26,41 @@ public interface IStartupLogger : ILogger
|
||||
/// <param name="logEntry">Defines the log message that introduces the new group.</param>
|
||||
/// <returns>A new logger that can write to the group.</returns>
|
||||
IStartupLogger BeginGroup(FormattableString logEntry);
|
||||
|
||||
/// <summary>
|
||||
/// Adds another logger instance to this logger for combined logging.
|
||||
/// </summary>
|
||||
/// <param name="logger">Other logger to rely messages to.</param>
|
||||
/// <returns>A combined logger.</returns>
|
||||
/// <typeparam name="TCategory">The logger cateogry.</typeparam>
|
||||
IStartupLogger<TCategory> With<TCategory>(ILogger logger);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a new Group logger within the parent logger.
|
||||
/// </summary>
|
||||
/// <param name="logEntry">Defines the log message that introduces the new group.</param>
|
||||
/// <returns>A new logger that can write to the group.</returns>
|
||||
/// <typeparam name="TCategory">The logger cateogry.</typeparam>
|
||||
IStartupLogger<TCategory> BeginGroup<TCategory>(FormattableString logEntry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a logger that can be injected via DI to get a startup logger initialised with an logger framework connected <see cref="ILogger"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TCategory">The logger cateogry.</typeparam>
|
||||
public interface IStartupLogger<TCategory> : IStartupLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds another logger instance to this logger for combined logging.
|
||||
/// </summary>
|
||||
/// <param name="logger">Other logger to rely messages to.</param>
|
||||
/// <returns>A combined logger.</returns>
|
||||
new IStartupLogger<TCategory> With(ILogger logger);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a new Group logger within the parent logger.
|
||||
/// </summary>
|
||||
/// <param name="logEntry">Defines the log message that introduces the new group.</param>
|
||||
/// <returns>A new logger that can write to the group.</returns>
|
||||
new IStartupLogger<TCategory> BeginGroup(FormattableString logEntry);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Configuration;
|
||||
using Emby.Server.Implementations.Serialization;
|
||||
using Jellyfin.Networking.Manager;
|
||||
using Jellyfin.Server.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
@@ -18,6 +19,7 @@ using MediaBrowser.Model.System;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
@@ -71,7 +73,7 @@ public sealed class SetupServer : IDisposable
|
||||
_configurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
|
||||
}
|
||||
|
||||
internal static ConcurrentQueue<StartupLogEntry>? LogQueue { get; set; } = new();
|
||||
internal static ConcurrentQueue<StartupLogTopic>? LogQueue { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether Startup server is currently running.
|
||||
@@ -88,12 +90,12 @@ public sealed class SetupServer : IDisposable
|
||||
_startupUiRenderer = (await ParserOptionsBuilder.New()
|
||||
.WithTemplate(fileTemplate)
|
||||
.WithFormatter(
|
||||
(StartupLogEntry logEntry, IEnumerable<StartupLogEntry> children) =>
|
||||
(StartupLogTopic logEntry, IEnumerable<StartupLogTopic> children) =>
|
||||
{
|
||||
if (children.Any())
|
||||
{
|
||||
var maxLevel = logEntry.LogLevel;
|
||||
var stack = new Stack<StartupLogEntry>(children);
|
||||
var stack = new Stack<StartupLogTopic>(children);
|
||||
|
||||
while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) != null) // error is the highest inherted error level.
|
||||
{
|
||||
@@ -138,19 +140,23 @@ public sealed class SetupServer : IDisposable
|
||||
|
||||
ThrowIfDisposed();
|
||||
var retryAfterValue = TimeSpan.FromSeconds(5);
|
||||
_startupServer = Host.CreateDefaultBuilder()
|
||||
var config = _configurationManager.GetNetworkConfiguration()!;
|
||||
_startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"])
|
||||
.UseConsoleLifetime()
|
||||
.ConfigureServices(serv =>
|
||||
{
|
||||
serv.AddHealthChecks()
|
||||
.AddCheck<SetupHealthcheck>("StartupCheck");
|
||||
serv.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
ApiServiceCollectionExtensions.ConfigureForwardHeaders(config, options);
|
||||
});
|
||||
})
|
||||
.ConfigureWebHostDefaults(webHostBuilder =>
|
||||
{
|
||||
webHostBuilder
|
||||
.UseKestrel((builderContext, options) =>
|
||||
{
|
||||
var config = _configurationManager.GetNetworkConfiguration()!;
|
||||
var knownBindInterfaces = NetworkManager.GetInterfacesCore(_loggerFactory.CreateLogger<SetupServer>(), config.EnableIPv4, config.EnableIPv6);
|
||||
knownBindInterfaces = NetworkManager.FilterBindSettings(config, knownBindInterfaces.ToList(), config.EnableIPv4, config.EnableIPv6);
|
||||
var bindInterfaces = NetworkManager.GetAllBindInterfaces(false, _configurationManager, knownBindInterfaces, config.EnableIPv4, config.EnableIPv6);
|
||||
@@ -168,7 +174,7 @@ public sealed class SetupServer : IDisposable
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseHealthChecks("/health");
|
||||
|
||||
app.UseForwardedHeaders();
|
||||
app.Map("/startup/logger", loggerRoute =>
|
||||
{
|
||||
loggerRoute.Run(async context =>
|
||||
@@ -362,15 +368,4 @@ public sealed class SetupServer : IDisposable
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal class StartupLogEntry
|
||||
{
|
||||
public LogLevel LogLevel { get; set; }
|
||||
|
||||
public string? Content { get; set; }
|
||||
|
||||
public DateTimeOffset DateOfCreation { get; set; }
|
||||
|
||||
public List<StartupLogEntry> Children { get; set; } = [];
|
||||
}
|
||||
}
|
||||
|
||||
31
Jellyfin.Server/ServerSetupApp/StartupLogTopic.cs
Normal file
31
Jellyfin.Server/ServerSetupApp/StartupLogTopic.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.ServerSetupApp;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a topic for the Startup UI.
|
||||
/// </summary>
|
||||
public class StartupLogTopic
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or Sets the LogLevel.
|
||||
/// </summary>
|
||||
public LogLevel LogLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the descriptor for the topic.
|
||||
/// </summary>
|
||||
public string? Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time the topic was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset DateOfCreation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the child items of this topic.
|
||||
/// </summary>
|
||||
public Collection<StartupLogTopic> Children { get; } = [];
|
||||
}
|
||||
@@ -1,56 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Server.Migrations.Routines;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Jellyfin.Server.ServerSetupApp;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public class StartupLogger : IStartupLogger
|
||||
{
|
||||
private readonly SetupServer.StartupLogEntry? _groupEntry;
|
||||
private readonly StartupLogTopic? _topic;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StartupLogger"/> class.
|
||||
/// </summary>
|
||||
public StartupLogger()
|
||||
/// <param name="logger">The underlying base logger.</param>
|
||||
public StartupLogger(ILogger logger)
|
||||
{
|
||||
Loggers = [];
|
||||
BaseLogger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StartupLogger"/> class.
|
||||
/// </summary>
|
||||
private StartupLogger(SetupServer.StartupLogEntry? groupEntry) : this()
|
||||
/// <param name="logger">The underlying base logger.</param>
|
||||
/// <param name="topic">The group for this logger.</param>
|
||||
internal StartupLogger(ILogger logger, StartupLogTopic? topic) : this(logger)
|
||||
{
|
||||
_groupEntry = groupEntry;
|
||||
_topic = topic;
|
||||
}
|
||||
|
||||
internal static IStartupLogger Logger { get; } = new StartupLogger();
|
||||
internal static IStartupLogger Logger { get; set; } = new StartupLogger(NullLogger.Instance);
|
||||
|
||||
private List<ILogger> Loggers { get; set; }
|
||||
/// <inheritdoc/>
|
||||
public StartupLogTopic? Topic => _topic;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the underlying base logger.
|
||||
/// </summary>
|
||||
protected ILogger BaseLogger { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IStartupLogger BeginGroup(FormattableString logEntry)
|
||||
{
|
||||
var startupEntry = new SetupServer.StartupLogEntry()
|
||||
return new StartupLogger(BaseLogger, AddToTopic(logEntry));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IStartupLogger With(ILogger logger)
|
||||
{
|
||||
return new StartupLogger(logger, Topic);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IStartupLogger<TCategory> With<TCategory>(ILogger logger)
|
||||
{
|
||||
return new StartupLogger<TCategory>(logger, Topic);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IStartupLogger<TCategory> BeginGroup<TCategory>(FormattableString logEntry)
|
||||
{
|
||||
return new StartupLogger<TCategory>(BaseLogger, AddToTopic(logEntry));
|
||||
}
|
||||
|
||||
private StartupLogTopic AddToTopic(FormattableString logEntry)
|
||||
{
|
||||
var startupEntry = new StartupLogTopic()
|
||||
{
|
||||
Content = logEntry.ToString(CultureInfo.InvariantCulture),
|
||||
DateOfCreation = DateTimeOffset.Now
|
||||
};
|
||||
|
||||
if (_groupEntry is null)
|
||||
if (Topic is null)
|
||||
{
|
||||
SetupServer.LogQueue?.Enqueue(startupEntry);
|
||||
}
|
||||
else
|
||||
{
|
||||
_groupEntry.Children.Add(startupEntry);
|
||||
Topic.Children.Add(startupEntry);
|
||||
}
|
||||
|
||||
return new StartupLogger(startupEntry);
|
||||
return startupEntry;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -69,34 +99,26 @@ public class StartupLogger : IStartupLogger
|
||||
/// <inheritdoc/>
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
foreach (var item in Loggers.Where(e => e.IsEnabled(logLevel)))
|
||||
if (BaseLogger.IsEnabled(logLevel))
|
||||
{
|
||||
item.Log(logLevel, eventId, state, exception, formatter);
|
||||
// if enabled allow the base logger also to receive the message
|
||||
BaseLogger.Log(logLevel, eventId, state, exception, formatter);
|
||||
}
|
||||
|
||||
var startupEntry = new SetupServer.StartupLogEntry()
|
||||
var startupEntry = new StartupLogTopic()
|
||||
{
|
||||
LogLevel = logLevel,
|
||||
Content = formatter(state, exception),
|
||||
DateOfCreation = DateTimeOffset.Now
|
||||
};
|
||||
|
||||
if (_groupEntry is null)
|
||||
if (Topic is null)
|
||||
{
|
||||
SetupServer.LogQueue?.Enqueue(startupEntry);
|
||||
}
|
||||
else
|
||||
{
|
||||
_groupEntry.Children.Add(startupEntry);
|
||||
Topic.Children.Add(startupEntry);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IStartupLogger With(ILogger logger)
|
||||
{
|
||||
return new StartupLogger(_groupEntry)
|
||||
{
|
||||
Loggers = [.. Loggers, logger]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
18
Jellyfin.Server/ServerSetupApp/StartupLoggerExtensions.cs
Normal file
18
Jellyfin.Server/ServerSetupApp/StartupLoggerExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Jellyfin.Server.ServerSetupApp;
|
||||
|
||||
internal static class StartupLoggerExtensions
|
||||
{
|
||||
public static IServiceCollection RegisterStartupLogger(this IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.AddTransient<IStartupLogger, StartupLogger<Startup>>()
|
||||
.AddTransient(typeof(IStartupLogger<>), typeof(StartupLogger<>));
|
||||
}
|
||||
}
|
||||
56
Jellyfin.Server/ServerSetupApp/StartupLoggerOfCategory.cs
Normal file
56
Jellyfin.Server/ServerSetupApp/StartupLoggerOfCategory.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Server.ServerSetupApp;
|
||||
|
||||
/// <summary>
|
||||
/// Startup logger for usage with DI that utilises an underlying logger from the DI.
|
||||
/// </summary>
|
||||
/// <typeparam name="TCategory">The category of the underlying logger.</typeparam>
|
||||
#pragma warning disable SA1649 // File name should match first type name
|
||||
public class StartupLogger<TCategory> : StartupLogger, IStartupLogger<TCategory>
|
||||
#pragma warning restore SA1649 // File name should match first type name
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StartupLogger{TCategory}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The injected base logger.</param>
|
||||
public StartupLogger(ILogger<TCategory> logger) : base(logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StartupLogger{TCategory}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The underlying base logger.</param>
|
||||
/// <param name="groupEntry">The group for this logger.</param>
|
||||
internal StartupLogger(ILogger logger, StartupLogTopic? groupEntry) : base(logger, groupEntry)
|
||||
{
|
||||
}
|
||||
|
||||
IStartupLogger<TCategory> IStartupLogger<TCategory>.BeginGroup(FormattableString logEntry)
|
||||
{
|
||||
var startupEntry = new StartupLogTopic()
|
||||
{
|
||||
Content = logEntry.ToString(CultureInfo.InvariantCulture),
|
||||
DateOfCreation = DateTimeOffset.Now
|
||||
};
|
||||
|
||||
if (Topic is null)
|
||||
{
|
||||
SetupServer.LogQueue?.Enqueue(startupEntry);
|
||||
}
|
||||
else
|
||||
{
|
||||
Topic.Children.Add(startupEntry);
|
||||
}
|
||||
|
||||
return new StartupLogger<TCategory>(BaseLogger, startupEntry);
|
||||
}
|
||||
|
||||
IStartupLogger<TCategory> IStartupLogger<TCategory>.With(ILogger logger)
|
||||
{
|
||||
return new StartupLogger<TCategory>(logger, Topic);
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,7 @@
|
||||
</li>
|
||||
{{--| /DECLARE}}
|
||||
|
||||
{{#IF localNetworkRequest}}
|
||||
<div class="flex-col">
|
||||
<ol class="action-list">
|
||||
{{#FOREACH log IN logs.Reverse()}}
|
||||
@@ -211,6 +212,10 @@
|
||||
{{/FOREACH}}
|
||||
</ol>
|
||||
</div>
|
||||
{{#ELSE}}
|
||||
<p>Please visit this page from your local network to view detailed startup logs.</p>
|
||||
{{/ELSE}}
|
||||
{{/IF}}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -88,6 +88,9 @@ EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.LiveTv", "src\Jellyfin.LiveTv\Jellyfin.LiveTv.csproj", "{8C6B2B13-58A4-4506-9DAB-1F882A093FE0}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jellyfin.Database", "Jellyfin.Database", "{4C54CE05-69C8-48FA-8785-39F7F6DB1CAD}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
src\Jellyfin.Database\readme.md = src\Jellyfin.Database\readme.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Providers.Sqlite", "src\Jellyfin.Database\Jellyfin.Database.Providers.Sqlite\Jellyfin.Database.Providers.Sqlite.csproj", "{A5590358-33CC-4B39-BDE7-DC62FEB03C76}"
|
||||
EndProject
|
||||
|
||||
@@ -2002,9 +2002,10 @@ namespace MediaBrowser.Controller.Entities
|
||||
}
|
||||
|
||||
// Remove from file system
|
||||
if (info.IsLocalFile)
|
||||
var path = info.Path;
|
||||
if (info.IsLocalFile && !string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
FileSystem.DeleteFile(info.Path);
|
||||
FileSystem.DeleteFile(path);
|
||||
}
|
||||
|
||||
// Remove from item
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Security;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks.Dataflow;
|
||||
using J2N.Collections.Generic.Extensions;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Data.Enums;
|
||||
@@ -25,6 +24,7 @@ using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LibraryTaskScheduler;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -49,6 +49,8 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
public static IUserViewManager UserViewManager { get; set; }
|
||||
|
||||
public static ILimitedConcurrencyLibraryScheduler LimitedConcurrencyLibraryScheduler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is root.
|
||||
/// </summary>
|
||||
@@ -598,51 +600,13 @@ namespace MediaBrowser.Controller.Entities
|
||||
/// <returns>Task.</returns>
|
||||
private async Task RunTasks<T>(Func<T, IProgress<double>, Task> task, IList<T> children, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var childrenCount = children.Count;
|
||||
var childrenProgress = new double[childrenCount];
|
||||
|
||||
void UpdateProgress()
|
||||
{
|
||||
progress.Report(childrenProgress.Average());
|
||||
}
|
||||
|
||||
var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
|
||||
var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount;
|
||||
|
||||
var actionBlock = new ActionBlock<int>(
|
||||
async i =>
|
||||
{
|
||||
var innerProgress = new Progress<double>(innerPercent =>
|
||||
{
|
||||
// round the percent and only update progress if it changed to prevent excessive UpdateProgress calls
|
||||
var innerPercentRounded = Math.Round(innerPercent);
|
||||
if (childrenProgress[i] != innerPercentRounded)
|
||||
{
|
||||
childrenProgress[i] = innerPercentRounded;
|
||||
UpdateProgress();
|
||||
}
|
||||
});
|
||||
|
||||
await task(children[i], innerProgress).ConfigureAwait(false);
|
||||
|
||||
childrenProgress[i] = 100;
|
||||
|
||||
UpdateProgress();
|
||||
},
|
||||
new ExecutionDataflowBlockOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = parallelism,
|
||||
CancellationToken = cancellationToken,
|
||||
});
|
||||
|
||||
for (var i = 0; i < childrenCount; i++)
|
||||
{
|
||||
await actionBlock.SendAsync(i, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
actionBlock.Complete();
|
||||
|
||||
await actionBlock.Completion.ConfigureAwait(false);
|
||||
await LimitedConcurrencyLibraryScheduler
|
||||
.Enqueue(
|
||||
children.ToArray(),
|
||||
task,
|
||||
progress,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1008,7 +972,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager);
|
||||
}
|
||||
|
||||
#pragma warning disable CA1309
|
||||
#pragma warning disable CA1309
|
||||
if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater))
|
||||
{
|
||||
items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.InvariantCultureIgnoreCase) < 1);
|
||||
@@ -1023,7 +987,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultureIgnoreCase) == 1);
|
||||
}
|
||||
#pragma warning restore CA1309
|
||||
#pragma warning restore CA1309
|
||||
|
||||
// This must be the last filter
|
||||
if (!query.AdjacentTo.IsNullOrEmpty())
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
|
||||
namespace MediaBrowser.Controller.LibraryTaskScheduler;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a shared scheduler to run library related tasks based on the <see cref="ServerConfiguration.LibraryScanFanoutConcurrency"/>.
|
||||
/// </summary>
|
||||
public interface ILimitedConcurrencyLibraryScheduler
|
||||
{
|
||||
/// <summary>
|
||||
/// Enqueues an action that will be invoked with the set data.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data Type.</typeparam>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <param name="worker">The callback to process the data.</param>
|
||||
/// <param name="progress">A progress reporter.</param>
|
||||
/// <param name="cancellationToken">Stop token.</param>
|
||||
/// <returns>A task that finishes when all data has been processed by the worker.</returns>
|
||||
Task Enqueue<T>(T[] data, Func<T, IProgress<double>, Task> worker, IProgress<double> progress, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.LibraryTaskScheduler;
|
||||
|
||||
/// <summary>
|
||||
/// Provides Parallel action interface to process tasks with a set concurrency level.
|
||||
/// </summary>
|
||||
public sealed class LimitedConcurrencyLibraryScheduler : ILimitedConcurrencyLibraryScheduler, IAsyncDisposable
|
||||
{
|
||||
private const int CleanupGracePeriod = 60;
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private readonly ILogger<LimitedConcurrencyLibraryScheduler> _logger;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly Dictionary<CancellationTokenSource, Task> _taskRunners = new();
|
||||
|
||||
private static readonly AsyncLocal<CancellationTokenSource> _deadlockDetector = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets used to lock all operations on the Tasks queue and creating workers.
|
||||
/// </summary>
|
||||
private readonly Lock _taskLock = new();
|
||||
|
||||
private readonly BlockingCollection<TaskQueueItem> _tasks = new();
|
||||
|
||||
private volatile int _workCounter;
|
||||
private Task? _cleanupTask;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LimitedConcurrencyLibraryScheduler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="hostApplicationLifetime">The hosting lifetime.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="serverConfigurationManager">The server configuration manager.</param>
|
||||
public LimitedConcurrencyLibraryScheduler(
|
||||
IHostApplicationLifetime hostApplicationLifetime,
|
||||
ILogger<LimitedConcurrencyLibraryScheduler> logger,
|
||||
IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_hostApplicationLifetime = hostApplicationLifetime;
|
||||
_logger = logger;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
private void ScheduleTaskCleanup()
|
||||
{
|
||||
lock (_taskLock)
|
||||
{
|
||||
if (_cleanupTask is not null)
|
||||
{
|
||||
_logger.LogDebug("Cleanup task already scheduled.");
|
||||
// cleanup task is already running.
|
||||
return;
|
||||
}
|
||||
|
||||
_cleanupTask = RunCleanupTask();
|
||||
}
|
||||
|
||||
async Task RunCleanupTask()
|
||||
{
|
||||
_logger.LogDebug("Schedule cleanup task in {CleanupGracePerioid} sec.", CleanupGracePeriod);
|
||||
await Task.Delay(TimeSpan.FromSeconds(CleanupGracePeriod)).ConfigureAwait(false);
|
||||
if (_disposed)
|
||||
{
|
||||
_logger.LogDebug("Abort cleaning up, already disposed.");
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_taskLock)
|
||||
{
|
||||
if (_tasks.Count > 0 || _workCounter > 0)
|
||||
{
|
||||
_logger.LogDebug("Delay cleanup task, operations still running.");
|
||||
// tasks are still there so its still in use. Reschedule cleanup task.
|
||||
// we cannot just exit here and rely on the other invoker because there is a considerable timeframe where it could have already ended.
|
||||
_cleanupTask = RunCleanupTask();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("Cleanup runners.");
|
||||
foreach (var item in _taskRunners.ToArray())
|
||||
{
|
||||
await item.Key.CancelAsync().ConfigureAwait(false);
|
||||
_taskRunners.Remove(item.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Worker()
|
||||
{
|
||||
lock (_taskLock)
|
||||
{
|
||||
var fanoutConcurrency = _serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency;
|
||||
var parallelism = (fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount) - _taskRunners.Count;
|
||||
_logger.LogDebug("Spawn {NumberRunners} new runners.", parallelism);
|
||||
for (int i = 0; i < parallelism; i++)
|
||||
{
|
||||
var stopToken = new CancellationTokenSource();
|
||||
var combinedSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken.Token, _hostApplicationLifetime.ApplicationStopping);
|
||||
_taskRunners.Add(
|
||||
combinedSource,
|
||||
Task.Factory.StartNew(
|
||||
ItemWorker,
|
||||
(combinedSource, stopToken),
|
||||
combinedSource.Token,
|
||||
TaskCreationOptions.PreferFairness,
|
||||
TaskScheduler.Default));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ItemWorker(object? obj)
|
||||
{
|
||||
var stopToken = ((CancellationTokenSource TaskStop, CancellationTokenSource GlobalStop))obj!;
|
||||
_deadlockDetector.Value = stopToken.TaskStop;
|
||||
try
|
||||
{
|
||||
foreach (var item in _tasks.GetConsumingEnumerable(stopToken.GlobalStop.Token))
|
||||
{
|
||||
stopToken.GlobalStop.Token.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
var newWorkerLimit = Interlocked.Increment(ref _workCounter) > 0;
|
||||
Debug.Assert(newWorkerLimit, "_workCounter > 0");
|
||||
_logger.LogDebug("Process new item '{Data}'.", item.Data);
|
||||
await ProcessItem(item).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
var newWorkerLimit = Interlocked.Decrement(ref _workCounter) >= 0;
|
||||
Debug.Assert(newWorkerLimit, "_workCounter > 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (stopToken.TaskStop.IsCancellationRequested)
|
||||
{
|
||||
// thats how you do it, interupt the waiter thread. There is nothing to do here when it was on purpose.
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogDebug("Cleanup Runner'.");
|
||||
_deadlockDetector.Value = default!;
|
||||
_taskRunners.Remove(stopToken.TaskStop);
|
||||
stopToken.GlobalStop.Dispose();
|
||||
stopToken.TaskStop.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessItem(TaskQueueItem item)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (item.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// if item is cancelled, just skip it
|
||||
return;
|
||||
}
|
||||
|
||||
await item.Worker(item.Data).ConfigureAwait(true);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while performing a library operation");
|
||||
}
|
||||
finally
|
||||
{
|
||||
item.Progress.Report(100);
|
||||
item.Done.SetResult();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task Enqueue<T>(T[] data, Func<T, IProgress<double>, Task> worker, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.Length == 0 || cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
progress.Report(100);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Enqueue new Workset of {NoItems} items.", data.Length);
|
||||
|
||||
TaskQueueItem[] workItems = null!;
|
||||
|
||||
void UpdateProgress()
|
||||
{
|
||||
progress.Report(workItems.Select(e => e.ProgressValue).Average());
|
||||
}
|
||||
|
||||
workItems = data.Select(item =>
|
||||
{
|
||||
TaskQueueItem queueItem = null!;
|
||||
return queueItem = new TaskQueueItem()
|
||||
{
|
||||
Data = item!,
|
||||
Progress = new Progress<double>(innerPercent =>
|
||||
{
|
||||
// round the percent and only update progress if it changed to prevent excessive UpdateProgress calls
|
||||
var innerPercentRounded = Math.Round(innerPercent);
|
||||
if (queueItem.ProgressValue != innerPercentRounded)
|
||||
{
|
||||
queueItem.ProgressValue = innerPercentRounded;
|
||||
UpdateProgress();
|
||||
}
|
||||
}),
|
||||
Worker = (val) => worker((T)val, queueItem.Progress),
|
||||
CancellationToken = cancellationToken
|
||||
};
|
||||
}).ToArray();
|
||||
|
||||
if (_serverConfigurationManager.Configuration.LibraryScanFanoutConcurrency == 1)
|
||||
{
|
||||
_logger.LogDebug("Process sequentially.");
|
||||
try
|
||||
{
|
||||
foreach (var item in workItems)
|
||||
{
|
||||
await ProcessItem(item).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// operation is cancelled. Do nothing.
|
||||
}
|
||||
|
||||
_logger.LogDebug("Process sequentially done.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < workItems.Length; i++)
|
||||
{
|
||||
var item = workItems[i]!;
|
||||
_tasks.Add(item, CancellationToken.None);
|
||||
}
|
||||
|
||||
if (_deadlockDetector.Value is not null)
|
||||
{
|
||||
_logger.LogDebug("Nested invocation detected, process in-place.");
|
||||
try
|
||||
{
|
||||
// we are in a nested loop. There is no reason to spawn a task here as that would just lead to deadlocks and no additional concurrency is achieved
|
||||
while (workItems.Any(e => !e.Done.Task.IsCompleted) && _tasks.TryTake(out var item, 200, _deadlockDetector.Value.Token))
|
||||
{
|
||||
await ProcessItem(item).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (_deadlockDetector.Value.IsCancellationRequested)
|
||||
{
|
||||
// operation is cancelled. Do nothing.
|
||||
}
|
||||
|
||||
_logger.LogDebug("process in-place done.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Worker();
|
||||
_logger.LogDebug("Wait for {NoWorkers} to complete.", workItems.Length);
|
||||
await Task.WhenAll([.. workItems.Select(f => f.Done.Task)]).ConfigureAwait(false);
|
||||
_logger.LogDebug("{NoWorkers} completed.", workItems.Length);
|
||||
ScheduleTaskCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_tasks.CompleteAdding();
|
||||
foreach (var item in _taskRunners)
|
||||
{
|
||||
await item.Key.CancelAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_tasks.Dispose();
|
||||
if (_cleanupTask is not null)
|
||||
{
|
||||
await _cleanupTask.ConfigureAwait(false);
|
||||
_cleanupTask?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private class TaskQueueItem
|
||||
{
|
||||
public required object Data { get; init; }
|
||||
|
||||
public double ProgressValue { get; set; }
|
||||
|
||||
public required Func<object, Task> Worker { get; init; }
|
||||
|
||||
public required IProgress<double> Progress { get; init; }
|
||||
|
||||
public TaskCompletionSource Done { get; } = new();
|
||||
|
||||
public CancellationToken CancellationToken { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -2376,6 +2376,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
var requestHasHDR10 = requestedRangeTypes.Contains(VideoRangeType.HDR10.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasHLG = requestedRangeTypes.Contains(VideoRangeType.HLG.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasSDR = requestedRangeTypes.Contains(VideoRangeType.SDR.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
var requestHasDOVI = requestedRangeTypes.Contains(VideoRangeType.DOVI.ToString(), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// If the client does not support DOVI and the video stream is DOVI without fallback, we should not copy it.
|
||||
if (!requestHasDOVI && videoStream.VideoRangeType == VideoRangeType.DOVI)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!requestedRangeTypes.Contains(videoStream.VideoRangeType.ToString(), StringComparison.OrdinalIgnoreCase)
|
||||
&& !((requestHasHDR10 && videoStream.VideoRangeType == VideoRangeType.DOVIWithHDR10)
|
||||
|
||||
@@ -20,10 +20,10 @@ public interface IMediaSegmentManager
|
||||
/// </summary>
|
||||
/// <param name="baseItem">The Item to evaluate.</param>
|
||||
/// <param name="libraryOptions">The library options.</param>
|
||||
/// <param name="overwrite">If set, will remove existing segments and replace it with new ones otherwise will check for existing segments and if found any, stops.</param>
|
||||
/// <param name="forceOverwrite">If set, will force to remove existing segments and replace it with new ones otherwise will check for existing segments and if found any that should not be deleted, stops.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that indicates the Operation is finished.</returns>
|
||||
Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool overwrite, CancellationToken cancellationToken);
|
||||
Task RunSegmentPluginProviders(BaseItem baseItem, LibraryOptions libraryOptions, bool forceOverwrite, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this item supports media segments.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Dto;
|
||||
@@ -102,4 +103,11 @@ public interface IItemRepository
|
||||
IReadOnlyList<string> GetGenreNames();
|
||||
|
||||
IReadOnlyList<string> GetAllArtistNames();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an item has been persisted to the database.
|
||||
/// </summary>
|
||||
/// <param name="id">The id to check.</param>
|
||||
/// <returns>True if the item exists, otherwise false.</returns>
|
||||
Task<bool> ItemExistsAsync(Guid id);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,12 @@ namespace MediaBrowser.Model.Dlna
|
||||
}
|
||||
}
|
||||
|
||||
var referenceBitrate = h264EquivalentOutputBitrate * (30.0f / (targetFps ?? 30.0f));
|
||||
// Our reference bitrate is based on SDR h264 at 30fps
|
||||
var referenceFps = targetFps ?? 30.0f;
|
||||
var referenceScale = referenceFps <= 30.0f
|
||||
? 30.0f / referenceFps
|
||||
: 1.0f / MathF.Sqrt(referenceFps / 30.0f);
|
||||
var referenceBitrate = h264EquivalentOutputBitrate * referenceScale;
|
||||
|
||||
if (isHdr)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Model.MediaSegments;
|
||||
|
||||
namespace MediaBrowser.Model;
|
||||
|
||||
@@ -11,4 +14,9 @@ public record MediaSegmentGenerationRequest
|
||||
/// Gets the Id to the BaseItem the segments should be extracted from.
|
||||
/// </summary>
|
||||
public Guid ItemId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets existing media segments generated on an earlier scan by this provider.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<MediaSegmentDto> ExistingSegments { get; init; }
|
||||
}
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.Session
|
||||
{
|
||||
public class QueueItem
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
namespace MediaBrowser.Model.Session;
|
||||
|
||||
public string PlaylistItemId { get; set; }
|
||||
}
|
||||
public record QueueItem
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string PlaylistItemId { get; set; }
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -24,14 +25,16 @@ public class AudioBookMetadataService : MetadataService<AudioBook, SongInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public AudioBookMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<AudioBookMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -24,14 +25,16 @@ public class BookMetadataService : MetadataService<Book, BookInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public BookMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<BookMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -27,14 +28,16 @@ public class BoxSetMetadataService : MetadataService<BoxSet, BoxSetInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public BoxSetMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<BoxSetMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -23,14 +24,16 @@ public class ChannelMetadataService : MetadataService<Channel, ItemLookupInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public ChannelMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<ChannelMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -23,14 +24,16 @@ public class CollectionFolderMetadataService : MetadataService<CollectionFolder,
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public CollectionFolderMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<CollectionFolderMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -23,14 +24,16 @@ public class FolderMetadataService : MetadataService<Folder, ItemLookupInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public FolderMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<FolderMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -23,14 +24,16 @@ public class UserViewMetadataService : MetadataService<UserView, ItemLookupInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public UserViewMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<UserViewMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -23,14 +24,16 @@ public class GenreMetadataService : MetadataService<Genre, ItemLookupInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public GenreMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<GenreMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -23,14 +24,16 @@ public class LiveTvMetadataService : MetadataService<LiveTvChannel, ItemLookupIn
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public LiveTvMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<LiveTvMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,6 +377,10 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
// Nothing to do, already gone
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
// Nothing to do, already gone
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Unable to delete {Image}", image.Path);
|
||||
|
||||
@@ -14,6 +14,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@@ -33,7 +34,8 @@ namespace MediaBrowser.Providers.Manager
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
{
|
||||
ServerConfigurationManager = serverConfigurationManager;
|
||||
Logger = logger;
|
||||
@@ -41,6 +43,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
FileSystem = fileSystem;
|
||||
LibraryManager = libraryManager;
|
||||
ExternalDataManager = externalDataManager;
|
||||
ItemRepository = itemRepository;
|
||||
ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
|
||||
}
|
||||
|
||||
@@ -58,6 +61,8 @@ namespace MediaBrowser.Providers.Manager
|
||||
|
||||
protected IExternalDataManager ExternalDataManager { get; }
|
||||
|
||||
protected IItemRepository ItemRepository { get; }
|
||||
|
||||
protected virtual bool EnableUpdatingPremiereDateFromChildren => false;
|
||||
|
||||
protected virtual bool EnableUpdatingGenresFromChildren => false;
|
||||
@@ -85,6 +90,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
var itemOfType = (TItemType)item;
|
||||
var updateType = ItemUpdateType.None;
|
||||
|
||||
var libraryOptions = LibraryManager.GetLibraryOptions(item);
|
||||
var isFirstRefresh = item.DateLastRefreshed.Date == DateTime.MinValue.Date;
|
||||
var hasRefreshedMetadata = true;
|
||||
@@ -141,7 +147,8 @@ namespace MediaBrowser.Providers.Manager
|
||||
Item = itemOfType
|
||||
};
|
||||
|
||||
var beforeSaveResult = BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType);
|
||||
var beforeSaveResult = await BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType)
|
||||
.ConfigureAwait(false);
|
||||
updateType |= beforeSaveResult;
|
||||
|
||||
updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false);
|
||||
@@ -262,14 +269,13 @@ namespace MediaBrowser.Providers.Manager
|
||||
|
||||
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
|
||||
{
|
||||
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
|
||||
if (result.Item.SupportsPeople && result.People is not null)
|
||||
{
|
||||
var baseItem = result.Item;
|
||||
|
||||
await LibraryManager.UpdatePeopleAsync(baseItem, result.People, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
|
||||
@@ -285,12 +291,20 @@ namespace MediaBrowser.Providers.Manager
|
||||
/// <param name="isFullRefresh">if set to <c>true</c> [is full refresh].</param>
|
||||
/// <param name="currentUpdateType">Type of the current update.</param>
|
||||
/// <returns>ItemUpdateType.</returns>
|
||||
private ItemUpdateType BeforeSave(TItemType item, bool isFullRefresh, ItemUpdateType currentUpdateType)
|
||||
private async Task<ItemUpdateType> BeforeSave(TItemType item, bool isFullRefresh, ItemUpdateType currentUpdateType)
|
||||
{
|
||||
var updateType = BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
|
||||
|
||||
updateType |= item.OnMetadataChanged();
|
||||
|
||||
if (updateType == ItemUpdateType.None)
|
||||
{
|
||||
if (!await ItemRepository.ItemExistsAsync(item.Id).ConfigureAwait(false))
|
||||
{
|
||||
return ItemUpdateType.MetadataImport;
|
||||
}
|
||||
}
|
||||
|
||||
return updateType;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -24,14 +25,16 @@ public class MovieMetadataService : MetadataService<Movie, MovieInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public MovieMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<MovieMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -25,14 +26,16 @@ public class TrailerMetadataService : MetadataService<Trailer, TrailerInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public TrailerMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<TrailerMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -29,14 +32,16 @@ public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public AlbumMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<AlbumMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -53,6 +58,16 @@ public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
|
||||
protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item)
|
||||
=> item.GetRecursiveChildren(i => i is Audio);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task AfterMetadataRefresh(MusicAlbum item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
base.AfterMetadataRefresh(item, refreshOptions, cancellationToken);
|
||||
|
||||
SetPeople(item);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
|
||||
{
|
||||
@@ -83,7 +98,6 @@ public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
|
||||
updateType |= SetArtistsFromSongs(item, songs);
|
||||
updateType |= SetAlbumArtistFromSongs(item, songs);
|
||||
updateType |= SetAlbumFromSongs(item, songs);
|
||||
updateType |= SetPeople(item);
|
||||
}
|
||||
|
||||
return updateType;
|
||||
@@ -178,10 +192,8 @@ public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
|
||||
}
|
||||
}
|
||||
|
||||
private ItemUpdateType SetPeople(MusicAlbum item)
|
||||
private void SetPeople(MusicAlbum item)
|
||||
{
|
||||
var updateType = ItemUpdateType.None;
|
||||
|
||||
if (item.AlbumArtists.Any() || item.Artists.Any())
|
||||
{
|
||||
var people = new List<PersonInfo>();
|
||||
@@ -205,10 +217,7 @@ public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo>
|
||||
}
|
||||
|
||||
LibraryManager.UpdatePeople(item, people);
|
||||
updateType |= ItemUpdateType.MetadataEdit;
|
||||
}
|
||||
|
||||
return updateType;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -4,6 +4,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -25,14 +26,16 @@ public class ArtistMetadataService : MetadataService<MusicArtist, ArtistInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public ArtistMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<ArtistMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -26,14 +27,16 @@ public class AudioMetadataService : MetadataService<Audio, SongInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public AudioMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<AudioMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -26,14 +27,16 @@ public class MusicVideoMetadataService : MetadataService<MusicVideo, MusicVideoI
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public MusicVideoMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<MusicVideoMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -23,14 +24,16 @@ public class MusicGenreMetadataService : MetadataService<MusicGenre, ItemLookupI
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public MusicGenreMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<MusicGenreMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -23,14 +24,16 @@ public class PersonMetadataService : MetadataService<Person, PersonLookupInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public PersonMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<PersonMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -23,14 +24,16 @@ public class PhotoAlbumMetadataService : MetadataService<PhotoAlbum, ItemLookupI
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public PhotoAlbumMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<PhotoAlbumMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -23,14 +24,16 @@ public class PhotoMetadataService : MetadataService<Photo, ItemLookupInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public PhotoMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<PhotoMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@@ -27,14 +28,16 @@ public class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public PlaylistMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<PlaylistMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(result.strArtist))
|
||||
{
|
||||
item.AlbumArtists = new string[] { result.strArtist };
|
||||
item.AlbumArtists = [result.strArtist];
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result.intYearReleased))
|
||||
@@ -104,7 +104,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
|
||||
if (!string.IsNullOrEmpty(result.strGenre))
|
||||
{
|
||||
item.Genres = new[] { result.strGenre };
|
||||
item.Genres = [result.strGenre];
|
||||
}
|
||||
|
||||
item.SetProviderId(MetadataProvider.AudioDbArtist, result.idArtist);
|
||||
@@ -170,6 +170,11 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
var url = AudioDbArtistProvider.BaseUrl + "/album-mb.php?i=" + musicBrainzReleaseGroupId;
|
||||
|
||||
var path = GetAlbumInfoPath(_config.ApplicationPaths, musicBrainzReleaseGroupId);
|
||||
var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
||||
if (fileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -23,14 +24,16 @@ public class StudioMetadataService : MetadataService<Studio, ItemLookupInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public StudioMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<StudioMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -25,14 +26,16 @@ public class EpisodeMetadataService : MetadataService<Episode, EpisodeInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public EpisodeMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<EpisodeMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -28,14 +29,16 @@ public class SeasonMetadataService : MetadataService<Season, SeasonInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public SeasonMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<SeasonMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
@@ -36,6 +37,7 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public SeriesMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<SeriesMetadataService> logger,
|
||||
@@ -43,8 +45,9 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
ILocalizationManager localizationManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
_localizationManager = localizationManager;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -23,14 +24,16 @@ public class VideoMetadataService : MetadataService<Video, ItemLookupInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public VideoMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<VideoMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
@@ -23,14 +24,16 @@ public class YearMetadataService : MetadataService<Year, ItemLookupInfo>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="externalDataManager">Instance of the <see cref="IExternalDataManager"/> interface.</param>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
public YearMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<YearMetadataService> logger,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IExternalDataManager externalDataManager)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager)
|
||||
IExternalDataManager externalDataManager,
|
||||
IItemRepository itemRepository)
|
||||
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager, externalDataManager, itemRepository)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,11 @@ public class UserData
|
||||
/// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
|
||||
public bool? Likes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the date the referenced <see cref="Item"/> has been deleted.
|
||||
/// </summary>
|
||||
public DateTime? RetentionDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the key.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
@@ -53,5 +54,12 @@ public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
|
||||
builder.HasIndex(e => new { e.IsFolder, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated });
|
||||
// resume
|
||||
builder.HasIndex(e => new { e.MediaType, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey });
|
||||
|
||||
builder.HasData(new BaseItemEntity()
|
||||
{
|
||||
Id = Guid.Parse("00000000-0000-0000-0000-000000000001"),
|
||||
Type = "PLACEHOLDER",
|
||||
Name = "This is a placeholder item for UserData that has been detacted from its original item",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@ public class UserDataConfiguration : IEntityTypeConfiguration<UserData>
|
||||
builder.HasIndex(d => new { d.ItemId, d.UserId, d.PlaybackPositionTicks });
|
||||
builder.HasIndex(d => new { d.ItemId, d.UserId, d.IsFavorite });
|
||||
builder.HasIndex(d => new { d.ItemId, d.UserId, d.LastPlayedDate });
|
||||
builder.HasOne(e => e.Item);
|
||||
builder.HasOne(e => e.Item).WithMany(e => e.UserData);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class DetachUserDataInsteadOfDelete : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "RetentionDate",
|
||||
table: "UserData",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "BaseItems",
|
||||
columns: new[] { "Id", "Album", "AlbumArtists", "Artists", "Audio", "ChannelId", "CleanName", "CommunityRating", "CriticRating", "CustomRating", "Data", "DateCreated", "DateLastMediaAdded", "DateLastRefreshed", "DateLastSaved", "DateModified", "EndDate", "EpisodeTitle", "ExternalId", "ExternalSeriesId", "ExternalServiceId", "ExtraIds", "ExtraType", "ForcedSortName", "Genres", "Height", "IndexNumber", "InheritedParentalRatingSubValue", "InheritedParentalRatingValue", "IsFolder", "IsInMixedFolder", "IsLocked", "IsMovie", "IsRepeat", "IsSeries", "IsVirtualItem", "LUFS", "MediaType", "Name", "NormalizationGain", "OfficialRating", "OriginalTitle", "Overview", "OwnerId", "ParentId", "ParentIndexNumber", "Path", "PreferredMetadataCountryCode", "PreferredMetadataLanguage", "PremiereDate", "PresentationUniqueKey", "PrimaryVersionId", "ProductionLocations", "ProductionYear", "RunTimeTicks", "SeasonId", "SeasonName", "SeriesId", "SeriesName", "SeriesPresentationUniqueKey", "ShowId", "Size", "SortName", "StartDate", "Studios", "Tagline", "Tags", "TopParentId", "TotalBitrate", "Type", "UnratedType", "Width" },
|
||||
values: new object[] { new Guid("00000000-0000-0000-0000-000000000001"), null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, false, false, false, false, false, false, false, null, null, "This is a placeholder item for UserData that has been detacted from its original item", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "PLACEHOLDER", null, null });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RetentionDate",
|
||||
table: "UserData");
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "BaseItems",
|
||||
keyColumn: "Id",
|
||||
keyValue: new Guid("00000000-0000-0000-0000-000000000001"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b =>
|
||||
{
|
||||
@@ -392,6 +392,21 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.ToTable("BaseItems");
|
||||
|
||||
b.HasAnnotation("Sqlite:UseSqlReturningClause", false);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = new Guid("00000000-0000-0000-0000-000000000001"),
|
||||
IsFolder = false,
|
||||
IsInMixedFolder = false,
|
||||
IsLocked = false,
|
||||
IsMovie = false,
|
||||
IsRepeat = false,
|
||||
IsSeries = false,
|
||||
IsVirtualItem = false,
|
||||
Name = "This is a placeholder item for UserData that has been detacted from its original item",
|
||||
Type = "PLACEHOLDER"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b =>
|
||||
@@ -1373,6 +1388,9 @@ namespace Jellyfin.Server.Implementations.Migrations
|
||||
b.Property<double?>("Rating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<DateTime?>("RetentionDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SubtitleStreamIndex")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
||||
@@ -98,7 +98,10 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
.AddEnvironmentVariables("JELLYFIN_")
|
||||
.AddInMemoryCollection(commandLineOpts.ConvertToConfig());
|
||||
})
|
||||
.ConfigureServices(e => e.AddSingleton<IStartupLogger, NullStartupLogger>().AddSingleton(e));
|
||||
.ConfigureServices(e => e
|
||||
.AddSingleton<IStartupLogger, NullStartupLogger<object>>()
|
||||
.AddTransient(typeof(IStartupLogger<>), typeof(NullStartupLogger<>))
|
||||
.AddSingleton(e));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -132,13 +135,20 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private sealed class NullStartupLogger : IStartupLogger
|
||||
private sealed class NullStartupLogger<TCategory> : IStartupLogger<TCategory>
|
||||
{
|
||||
public StartupLogTopic? Topic => throw new NotImplementedException();
|
||||
|
||||
public IStartupLogger BeginGroup(FormattableString logEntry)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
public IStartupLogger<TCategory1> BeginGroup<TCategory1>(FormattableString logEntry)
|
||||
{
|
||||
return new NullStartupLogger<TCategory1>();
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state)
|
||||
where TState : notnull
|
||||
{
|
||||
@@ -160,10 +170,25 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
return this;
|
||||
}
|
||||
|
||||
public IStartupLogger<TCategory1> With<TCategory1>(Microsoft.Extensions.Logging.ILogger logger)
|
||||
{
|
||||
return new NullStartupLogger<TCategory1>();
|
||||
}
|
||||
|
||||
IStartupLogger<TCategory> IStartupLogger<TCategory>.BeginGroup(FormattableString logEntry)
|
||||
{
|
||||
return new NullStartupLogger<TCategory>();
|
||||
}
|
||||
|
||||
IStartupLogger IStartupLogger.With(Microsoft.Extensions.Logging.ILogger logger)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
IStartupLogger<TCategory> IStartupLogger<TCategory>.With(Microsoft.Extensions.Logging.ILogger logger)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user