mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-18 13:40:45 +01:00
Merge branch 'master' into sort-nfo-data
This commit is contained in:
@@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.BoxSets
|
||||
protected override bool EnableUpdatingPremiereDateFromChildren => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IList<BaseItem> GetChildrenForMetadataUpdates(BoxSet item)
|
||||
protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(BoxSet item)
|
||||
{
|
||||
return item.GetLinkedChildren();
|
||||
}
|
||||
@@ -54,7 +54,14 @@ namespace MediaBrowser.Providers.BoxSets
|
||||
|
||||
if (mergeMetadataSettings)
|
||||
{
|
||||
targetItem.LinkedChildren = sourceItem.LinkedChildren;
|
||||
if (replaceData || targetItem.LinkedChildren.Length == 0)
|
||||
{
|
||||
targetItem.LinkedChildren = sourceItem.LinkedChildren;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Providers.Chapters
|
||||
{
|
||||
public class ChapterManager : IChapterManager
|
||||
{
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
public ChapterManager(IItemRepository itemRepo)
|
||||
{
|
||||
_itemRepo = itemRepo;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters)
|
||||
{
|
||||
_itemRepo.SaveChapters(itemId, chapters);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ public class LyricScheduledTask : IScheduledTask
|
||||
[
|
||||
new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfo.TriggerInterval,
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromHours(24).Ticks
|
||||
}
|
||||
];
|
||||
|
||||
@@ -291,6 +291,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
|
||||
var fileStreamOptions = AsyncFile.WriteOptions;
|
||||
fileStreamOptions.Mode = FileMode.Create;
|
||||
fileStreamOptions.Options = FileOptions.WriteThrough;
|
||||
if (source.CanSeek)
|
||||
{
|
||||
fileStreamOptions.PreallocationSize = source.Length;
|
||||
|
||||
@@ -229,9 +229,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
var mimeType = MimeTypes.GetMimeType(response.Path);
|
||||
|
||||
var stream = AsyncFile.OpenRead(response.Path);
|
||||
|
||||
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
|
||||
await _providerManager.SaveImage(item, response.Path, mimeType, imageType, null, null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,8 +385,8 @@ namespace MediaBrowser.Providers.Manager
|
||||
|
||||
item.RemoveImages(images);
|
||||
|
||||
// Cleanup old metadata directory for episodes if empty
|
||||
if (item is Episode)
|
||||
// Cleanup old metadata directory for episodes if empty, as long as it's not a virtual item
|
||||
if (item is Episode && !item.IsVirtualItem)
|
||||
{
|
||||
var oldLocalMetadataDirectory = Path.Combine(item.ContainingFolderPath, "metadata");
|
||||
if (_fileSystem.DirectoryExists(oldLocalMetadataDirectory) && !_fileSystem.GetFiles(oldLocalMetadataDirectory).Any())
|
||||
|
||||
@@ -74,10 +74,11 @@ namespace MediaBrowser.Providers.Manager
|
||||
public virtual async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
var itemOfType = (TItemType)item;
|
||||
|
||||
var updateType = ItemUpdateType.None;
|
||||
|
||||
var libraryOptions = LibraryManager.GetLibraryOptions(item);
|
||||
var isFirstRefresh = item.DateLastRefreshed == default;
|
||||
var hasRefreshedMetadata = true;
|
||||
var hasRefreshedImages = true;
|
||||
|
||||
var requiresRefresh = libraryOptions.AutomaticRefreshIntervalDays > 0 && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= libraryOptions.AutomaticRefreshIntervalDays;
|
||||
|
||||
@@ -131,9 +132,10 @@ namespace MediaBrowser.Providers.Manager
|
||||
People = LibraryManager.GetPeople(item)
|
||||
};
|
||||
|
||||
bool hasRefreshedMetadata = true;
|
||||
bool hasRefreshedImages = true;
|
||||
var isFirstRefresh = item.DateLastRefreshed == default;
|
||||
var beforeSaveResult = BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType);
|
||||
updateType |= beforeSaveResult;
|
||||
|
||||
updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Next run metadata providers
|
||||
if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
|
||||
@@ -188,43 +190,43 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
}
|
||||
|
||||
var beforeSaveResult = BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType);
|
||||
updateType |= beforeSaveResult;
|
||||
|
||||
// Save if changes were made, or it's never been saved before
|
||||
if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.ReplaceAllMetadata || requiresRefresh)
|
||||
if (hasRefreshedMetadata && hasRefreshedImages)
|
||||
{
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
var file = TryGetFile(item.Path, refreshOptions.DirectoryService);
|
||||
if (file is not null)
|
||||
{
|
||||
item.DateModified = file.LastWriteTimeUtc;
|
||||
}
|
||||
}
|
||||
|
||||
// If any of these properties are set then make sure the updateType is not None, just to force everything to save
|
||||
if (refreshOptions.ForceSave || refreshOptions.ReplaceAllMetadata)
|
||||
{
|
||||
updateType |= ItemUpdateType.MetadataDownload;
|
||||
}
|
||||
|
||||
if (hasRefreshedMetadata && hasRefreshedImages)
|
||||
{
|
||||
item.DateLastRefreshed = DateTime.UtcNow;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.DateLastRefreshed = default;
|
||||
}
|
||||
|
||||
// Save to database
|
||||
await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
|
||||
item.DateLastRefreshed = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return updateType;
|
||||
|
||||
async Task<ItemUpdateType> SaveInternal(BaseItem item, MetadataRefreshOptions refreshOptions, ItemUpdateType updateType, bool isFirstRefresh, bool requiresRefresh, MetadataResult<TItemType> metadataResult, CancellationToken cancellationToken)
|
||||
{
|
||||
// Save if changes were made, or it's never been saved before
|
||||
if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.ReplaceAllMetadata || requiresRefresh)
|
||||
{
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
var file = TryGetFile(item.Path, refreshOptions.DirectoryService);
|
||||
if (file is not null)
|
||||
{
|
||||
item.DateModified = file.LastWriteTimeUtc;
|
||||
}
|
||||
}
|
||||
|
||||
// If any of these properties are set then make sure the updateType is not None, just to force everything to save
|
||||
if (refreshOptions.ForceSave || refreshOptions.ReplaceAllMetadata)
|
||||
{
|
||||
updateType |= ItemUpdateType.MetadataDownload;
|
||||
}
|
||||
|
||||
// Save to database
|
||||
await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return updateType;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySearchResult(ItemLookupInfo lookupInfo, RemoteSearchResult result)
|
||||
@@ -322,17 +324,17 @@ namespace MediaBrowser.Providers.Manager
|
||||
return false;
|
||||
}
|
||||
|
||||
protected virtual IList<BaseItem> GetChildrenForMetadataUpdates(TItemType item)
|
||||
protected virtual IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(TItemType item)
|
||||
{
|
||||
if (item is Folder folder)
|
||||
{
|
||||
return folder.GetRecursiveChildren();
|
||||
}
|
||||
|
||||
return Array.Empty<BaseItem>();
|
||||
return [];
|
||||
}
|
||||
|
||||
protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
|
||||
protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
|
||||
{
|
||||
var updateType = ItemUpdateType.None;
|
||||
|
||||
@@ -371,7 +373,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
return updateType;
|
||||
}
|
||||
|
||||
private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IList<BaseItem> children)
|
||||
private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IReadOnlyList<BaseItem> children)
|
||||
{
|
||||
if (item is Folder folder && folder.SupportsCumulativeRunTimeTicks)
|
||||
{
|
||||
@@ -395,7 +397,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
return ItemUpdateType.None;
|
||||
}
|
||||
|
||||
private ItemUpdateType UpdateDateLastMediaAdded(TItemType item, IList<BaseItem> children)
|
||||
private ItemUpdateType UpdateDateLastMediaAdded(TItemType item, IReadOnlyList<BaseItem> children)
|
||||
{
|
||||
var updateType = ItemUpdateType.None;
|
||||
|
||||
@@ -429,7 +431,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
return updateType;
|
||||
}
|
||||
|
||||
private ItemUpdateType UpdatePremiereDate(TItemType item, IList<BaseItem> children)
|
||||
private ItemUpdateType UpdatePremiereDate(TItemType item, IReadOnlyList<BaseItem> children)
|
||||
{
|
||||
var updateType = ItemUpdateType.None;
|
||||
|
||||
@@ -467,7 +469,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
return updateType;
|
||||
}
|
||||
|
||||
private ItemUpdateType UpdateGenres(TItemType item, IList<BaseItem> children)
|
||||
private ItemUpdateType UpdateGenres(TItemType item, IReadOnlyList<BaseItem> children)
|
||||
{
|
||||
var updateType = ItemUpdateType.None;
|
||||
|
||||
@@ -488,7 +490,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
return updateType;
|
||||
}
|
||||
|
||||
private ItemUpdateType UpdateStudios(TItemType item, IList<BaseItem> children)
|
||||
private ItemUpdateType UpdateStudios(TItemType item, IReadOnlyList<BaseItem> children)
|
||||
{
|
||||
var updateType = ItemUpdateType.None;
|
||||
|
||||
@@ -509,7 +511,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
return updateType;
|
||||
}
|
||||
|
||||
private ItemUpdateType UpdateOfficialRating(TItemType item, IList<BaseItem> children)
|
||||
private ItemUpdateType UpdateOfficialRating(TItemType item, IReadOnlyList<BaseItem> children)
|
||||
{
|
||||
var updateType = ItemUpdateType.None;
|
||||
|
||||
@@ -675,6 +677,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
};
|
||||
temp.Item.Path = item.Path;
|
||||
temp.Item.Id = item.Id;
|
||||
temp.Item.ParentIndexNumber = item.ParentIndexNumber;
|
||||
temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
|
||||
temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
|
||||
|
||||
@@ -728,7 +731,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||
}
|
||||
|
||||
MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
|
||||
MergeData(localItem, temp, [], false, true);
|
||||
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
||||
|
||||
break;
|
||||
@@ -768,7 +771,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
if (!options.RemoveOldMetadata)
|
||||
{
|
||||
// Add existing metadata to provider result if it does not exist there
|
||||
MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
|
||||
MergeData(metadata, temp, [], false, false);
|
||||
}
|
||||
|
||||
if (isLocalLocked)
|
||||
@@ -837,7 +840,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
{
|
||||
result.Provider = provider.Name;
|
||||
|
||||
MergeData(result, temp, Array.Empty<MetadataField>(), replaceData, false);
|
||||
MergeData(result, temp, [], replaceData, false);
|
||||
MergeNewData(temp.Item, id);
|
||||
|
||||
refreshResult.UpdateType |= ItemUpdateType.MetadataDownload;
|
||||
@@ -1141,13 +1144,8 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
}
|
||||
|
||||
private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
|
||||
private static void MergePeople(IReadOnlyList<PersonInfo> source, IReadOnlyList<PersonInfo> target)
|
||||
{
|
||||
if (target is null)
|
||||
{
|
||||
target = new List<PersonInfo>();
|
||||
}
|
||||
|
||||
foreach (var person in target)
|
||||
{
|
||||
var normalizedName = person.Name.RemoveDiacritics();
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
@@ -47,7 +48,7 @@ namespace MediaBrowser.Providers.Manager
|
||||
/// </summary>
|
||||
public class ProviderManager : IProviderManager, IDisposable
|
||||
{
|
||||
private readonly object _refreshQueueLock = new();
|
||||
private readonly Lock _refreshQueueLock = new();
|
||||
private readonly ILogger<ProviderManager> _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILibraryMonitor _libraryMonitor;
|
||||
@@ -199,11 +200,19 @@ namespace MediaBrowser.Providers.Manager
|
||||
// TODO: Isolate this hack into the tvh plugin
|
||||
if (string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
// Special case for imagecache
|
||||
if (url.Contains("/imagecache/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
contentType = MediaTypeNames.Image.Png;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Deduce content type from file extension
|
||||
contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
|
||||
}
|
||||
|
||||
// Throw if we still can't determine the content type
|
||||
if (string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
throw new HttpRequestException("Invalid image received: contentType not set.", null, response.StatusCode);
|
||||
}
|
||||
@@ -251,15 +260,31 @@ namespace MediaBrowser.Providers.Manager
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
|
||||
public async Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
var fileStream = AsyncFile.OpenRead(source);
|
||||
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
|
||||
try
|
||||
{
|
||||
var fileStream = AsyncFile.OpenRead(source);
|
||||
await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger)
|
||||
.SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(source);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Source file {Source} not found or in use, skip removing", source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -1016,7 +1041,6 @@ namespace MediaBrowser.Providers.Manager
|
||||
/// <inheritdoc/>
|
||||
public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(itemId);
|
||||
if (itemId.IsEmpty())
|
||||
{
|
||||
throw new ArgumentException("Guid can't be empty", nameof(itemId));
|
||||
|
||||
@@ -23,15 +23,14 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="PlaylistsNET" />
|
||||
<PackageReference Include="z440.atl.core"/>
|
||||
<PackageReference Include="z440.atl.core" />
|
||||
<PackageReference Include="TMDbLib" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers -->
|
||||
|
||||
@@ -17,6 +17,7 @@ using MediaBrowser.Controller.Sorting;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -28,6 +29,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
public class AudioFileProber
|
||||
{
|
||||
private const char InternalValueSeparator = '\u001F';
|
||||
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
@@ -35,6 +37,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly LyricResolver _lyricResolver;
|
||||
private readonly ILyricManager _lyricManager;
|
||||
private readonly IMediaStreamRepository _mediaStreamRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AudioFileProber"/> class.
|
||||
@@ -46,6 +49,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param>
|
||||
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
|
||||
/// <param name="mediaStreamRepository">Instance of the <see cref="IMediaStreamRepository"/>.</param>
|
||||
public AudioFileProber(
|
||||
ILogger<AudioFileProber> logger,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
@@ -53,7 +57,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
IItemRepository itemRepo,
|
||||
ILibraryManager libraryManager,
|
||||
LyricResolver lyricResolver,
|
||||
ILyricManager lyricManager)
|
||||
ILyricManager lyricManager,
|
||||
IMediaStreamRepository mediaStreamRepository)
|
||||
{
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_itemRepo = itemRepo;
|
||||
@@ -62,7 +67,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_lyricResolver = lyricResolver;
|
||||
_lyricManager = lyricManager;
|
||||
_mediaStreamRepository = mediaStreamRepository;
|
||||
ATL.Settings.DisplayValueSeparator = InternalValueSeparator;
|
||||
ATL.Settings.UseFileNameWhenNoTitle = false;
|
||||
ATL.Settings.ID3v2_separatev2v3Values = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -146,7 +154,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
|
||||
|
||||
_itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken);
|
||||
_mediaStreamRepository.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -161,12 +169,13 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
var libraryOptions = _libraryManager.GetLibraryOptions(audio);
|
||||
Track track = new Track(audio.Path);
|
||||
|
||||
// ATL will fall back to filename as title when it does not understand the metadata
|
||||
if (track.MetadataFormats.All(mf => mf.Equals(ATL.Factory.UNKNOWN_FORMAT)))
|
||||
if (track.MetadataFormats
|
||||
.All(mf => string.Equals(mf.ShortName, "ID3v1", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
track.Title = mediaInfo.Name;
|
||||
_logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path);
|
||||
}
|
||||
|
||||
track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title;
|
||||
track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
|
||||
track.Year ??= mediaInfo.ProductionYear;
|
||||
track.TrackNumber ??= mediaInfo.IndexNumber;
|
||||
@@ -175,11 +184,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
|
||||
{
|
||||
var people = new List<PersonInfo>();
|
||||
var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator);
|
||||
var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? [] : track.AlbumArtist.Split(InternalValueSeparator);
|
||||
|
||||
if (libraryOptions.UseCustomTagDelimiters)
|
||||
{
|
||||
albumArtists = albumArtists.SelectMany(a => SplitWithCustomDelimiter(a, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray();
|
||||
albumArtists = albumArtists.SelectMany(a => SplitWithCustomDelimiter(a, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray();
|
||||
}
|
||||
|
||||
foreach (var albumArtist in albumArtists)
|
||||
@@ -206,12 +215,12 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
if (performers is null || performers.Length == 0)
|
||||
{
|
||||
performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator);
|
||||
performers = string.IsNullOrEmpty(track.Artist) ? [] : track.Artist.Split(InternalValueSeparator);
|
||||
}
|
||||
|
||||
if (libraryOptions.UseCustomTagDelimiters)
|
||||
{
|
||||
performers = performers.SelectMany(p => SplitWithCustomDelimiter(p, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray();
|
||||
performers = performers.SelectMany(p => SplitWithCustomDelimiter(p, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray();
|
||||
}
|
||||
|
||||
foreach (var performer in performers)
|
||||
@@ -310,11 +319,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
if (!audio.LockedFields.Contains(MetadataField.Genres))
|
||||
{
|
||||
var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator);
|
||||
var genres = string.IsNullOrEmpty(track.Genre) ? [] : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
|
||||
if (libraryOptions.UseCustomTagDelimiters)
|
||||
{
|
||||
genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray();
|
||||
genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray();
|
||||
}
|
||||
|
||||
genres = genres.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
@@ -345,7 +354,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|| track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag))
|
||||
&& !string.IsNullOrEmpty(musicBrainzArtistTag))
|
||||
{
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzArtistTag);
|
||||
var id = GetFirstMusicBrainzId(musicBrainzArtistTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +365,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag))
|
||||
&& !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag))
|
||||
{
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, musicBrainzReleaseArtistIdTag);
|
||||
var id = GetFirstMusicBrainzId(musicBrainzReleaseArtistIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,7 +376,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|| track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag))
|
||||
&& !string.IsNullOrEmpty(musicBrainzReleaseIdTag))
|
||||
{
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, musicBrainzReleaseIdTag);
|
||||
var id = GetFirstMusicBrainzId(musicBrainzReleaseIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +387,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag))
|
||||
&& !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag))
|
||||
{
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, musicBrainzReleaseGroupIdTag);
|
||||
var id = GetFirstMusicBrainzId(musicBrainzReleaseGroupIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,7 +398,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|| track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId))
|
||||
&& !string.IsNullOrEmpty(trackMbId))
|
||||
{
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
|
||||
var id = GetFirstMusicBrainzId(trackMbId, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist);
|
||||
audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,5 +453,18 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
// MusicBrainz IDs are multi-value tags, so we need to split them
|
||||
// However, our current provider can only have one single ID, which means we need to pick the first one
|
||||
private string? GetFirstMusicBrainzId(string tag, bool useCustomTagDelimiters, char[] tagDelimiters, string[] whitelist)
|
||||
{
|
||||
var val = tag.Split(InternalValueSeparator).FirstOrDefault();
|
||||
if (val is not null && useCustomTagDelimiters)
|
||||
{
|
||||
val = SplitWithCustomDelimiter(val, tagDelimiters, whitelist).FirstOrDefault();
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CA1826 // CA1826 Do not use Enumerable methods on Indexable collections.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -74,18 +74,17 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
return GetImage((Audio)item, imageStreams, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<DynamicImageResponse> GetImage(Audio item, List<MediaStream> imageStreams, CancellationToken cancellationToken)
|
||||
private async Task<DynamicImageResponse> GetImage(Audio item, IReadOnlyList<MediaStream> imageStreams, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = GetAudioImagePath(item);
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
var directoryName = Path.GetDirectoryName(path) ?? throw new InvalidOperationException($"Invalid path '{path}'");
|
||||
Directory.CreateDirectory(directoryName);
|
||||
var imageStream = imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).Contains("front", StringComparison.OrdinalIgnoreCase)) ??
|
||||
imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).Contains("cover", StringComparison.OrdinalIgnoreCase)) ??
|
||||
imageStreams.FirstOrDefault();
|
||||
|
||||
var imageStreamIndex = imageStream?.Index;
|
||||
|
||||
var tempFile = await _mediaEncoder.ExtractAudioImage(item.Path, imageStreamIndex, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
public class FFProbeVideoInfo
|
||||
{
|
||||
private readonly ILogger<FFProbeVideoInfo> _logger;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IItemRepository _itemRepo;
|
||||
private readonly IBlurayExaminer _blurayExaminer;
|
||||
@@ -39,11 +40,12 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
private readonly IEncodingManager _encodingManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ISubtitleManager _subtitleManager;
|
||||
private readonly IChapterManager _chapterManager;
|
||||
private readonly IChapterRepository _chapterManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly AudioResolver _audioResolver;
|
||||
private readonly SubtitleResolver _subtitleResolver;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaAttachmentRepository _mediaAttachmentRepository;
|
||||
private readonly IMediaStreamRepository _mediaStreamRepository;
|
||||
|
||||
public FFProbeVideoInfo(
|
||||
ILogger<FFProbeVideoInfo> logger,
|
||||
@@ -55,10 +57,12 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
IEncodingManager encodingManager,
|
||||
IServerConfigurationManager config,
|
||||
ISubtitleManager subtitleManager,
|
||||
IChapterManager chapterManager,
|
||||
IChapterRepository chapterManager,
|
||||
ILibraryManager libraryManager,
|
||||
AudioResolver audioResolver,
|
||||
SubtitleResolver subtitleResolver)
|
||||
SubtitleResolver subtitleResolver,
|
||||
IMediaAttachmentRepository mediaAttachmentRepository,
|
||||
IMediaStreamRepository mediaStreamRepository)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
@@ -73,6 +77,9 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
_libraryManager = libraryManager;
|
||||
_audioResolver = audioResolver;
|
||||
_subtitleResolver = subtitleResolver;
|
||||
_mediaAttachmentRepository = mediaAttachmentRepository;
|
||||
_mediaStreamRepository = mediaStreamRepository;
|
||||
_mediaStreamRepository = mediaStreamRepository;
|
||||
}
|
||||
|
||||
public async Task<ItemUpdateType> ProbeVideo<T>(
|
||||
@@ -268,11 +275,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle);
|
||||
|
||||
_itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken);
|
||||
_mediaStreamRepository.SaveMediaStreams(video.Id, mediaStreams, cancellationToken);
|
||||
|
||||
if (mediaAttachments.Any())
|
||||
{
|
||||
_itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
|
||||
_mediaAttachmentRepository.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
|
||||
}
|
||||
|
||||
if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh
|
||||
@@ -355,7 +362,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
blurayVideoStream.Codec = ffmpegVideoStream.Codec;
|
||||
blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate;
|
||||
blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width;
|
||||
blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height;
|
||||
blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Height : blurayVideoStream.Height;
|
||||
blurayVideoStream.ColorRange = ffmpegVideoStream.ColorRange;
|
||||
blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace;
|
||||
blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer;
|
||||
@@ -628,7 +635,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
{
|
||||
var runtime = video.RunTimeTicks.GetValueOrDefault();
|
||||
|
||||
// Only process files with a runtime higher than 0 and lower than 12h. The latter are likely corrupted.
|
||||
// Only process files with a runtime greater than 0 and less than 12h. The latter are likely corrupted.
|
||||
if (runtime < 0 || runtime > TimeSpan.FromHours(12).Ticks)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
|
||||
@@ -121,7 +121,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
mediaStream.Index = startIndex++;
|
||||
mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
|
||||
mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
|
||||
mediaStream.IsHearingImpaired = pathInfo.IsHearingImpaired || mediaStream.IsHearingImpaired;
|
||||
mediaStream.IsHearingImpaired = pathInfo.IsHearingImpaired || mediaStream.IsHearingImpaired.GetValueOrDefault();
|
||||
|
||||
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
|
||||
}
|
||||
|
||||
@@ -61,12 +61,14 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
/// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param>
|
||||
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="subtitleManager">Instance of the <see cref="ISubtitleManager"/> interface.</param>
|
||||
/// <param name="chapterManager">Instance of the <see cref="IChapterManager"/> interface.</param>
|
||||
/// <param name="chapterManager">Instance of the <see cref="IChapterRepository"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/>.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
|
||||
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
|
||||
/// <param name="mediaAttachmentRepository">Instance of the <see cref="IMediaAttachmentRepository"/> interface.</param>
|
||||
/// <param name="mediaStreamRepository">Instance of the <see cref="IMediaStreamRepository"/> interface.</param>
|
||||
public ProbeProvider(
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
@@ -76,12 +78,14 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
IEncodingManager encodingManager,
|
||||
IServerConfigurationManager config,
|
||||
ISubtitleManager subtitleManager,
|
||||
IChapterManager chapterManager,
|
||||
IChapterRepository chapterManager,
|
||||
ILibraryManager libraryManager,
|
||||
IFileSystem fileSystem,
|
||||
ILoggerFactory loggerFactory,
|
||||
NamingOptions namingOptions,
|
||||
ILyricManager lyricManager)
|
||||
ILyricManager lyricManager,
|
||||
IMediaAttachmentRepository mediaAttachmentRepository,
|
||||
IMediaStreamRepository mediaStreamRepository)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<ProbeProvider>();
|
||||
_audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions);
|
||||
@@ -101,7 +105,9 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
chapterManager,
|
||||
libraryManager,
|
||||
_audioResolver,
|
||||
_subtitleResolver);
|
||||
_subtitleResolver,
|
||||
mediaAttachmentRepository,
|
||||
mediaStreamRepository);
|
||||
|
||||
_audioProber = new AudioFileProber(
|
||||
loggerFactory.CreateLogger<AudioFileProber>(),
|
||||
@@ -110,7 +116,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
itemRepo,
|
||||
libraryManager,
|
||||
_lyricResolver,
|
||||
lyricManager);
|
||||
lyricManager,
|
||||
mediaStreamRepository);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
public async Task<List<string>> DownloadSubtitles(
|
||||
Video video,
|
||||
List<MediaStream> mediaStreams,
|
||||
IReadOnlyList<MediaStream> mediaStreams,
|
||||
bool skipIfEmbeddedSubtitlesPresent,
|
||||
bool skipIfAudioTrackMatches,
|
||||
bool requirePerfectMatch,
|
||||
@@ -68,7 +68,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
public Task<bool> DownloadSubtitles(
|
||||
Video video,
|
||||
List<MediaStream> mediaStreams,
|
||||
IReadOnlyList<MediaStream> mediaStreams,
|
||||
bool skipIfEmbeddedSubtitlesPresent,
|
||||
bool skipIfAudioTrackMatches,
|
||||
bool requirePerfectMatch,
|
||||
@@ -120,7 +120,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
private async Task<bool> DownloadSubtitles(
|
||||
Video video,
|
||||
List<MediaStream> mediaStreams,
|
||||
IReadOnlyList<MediaStream> mediaStreams,
|
||||
bool skipIfEmbeddedSubtitlesPresent,
|
||||
bool skipIfAudioTrackMatches,
|
||||
bool requirePerfectMatch,
|
||||
|
||||
@@ -217,7 +217,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
return new[]
|
||||
{
|
||||
// Every so often
|
||||
new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
|
||||
new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#pragma warning disable CA1826 // CA1826 Do not use Enumerable methods on Indexable collections.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -47,11 +47,11 @@ namespace MediaBrowser.Providers.Music
|
||||
protected override bool EnableUpdatingStudiosFromChildren => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item)
|
||||
protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item)
|
||||
=> item.GetRecursiveChildren(i => i is Audio);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
|
||||
protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
|
||||
{
|
||||
var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
@@ -28,7 +29,7 @@ namespace MediaBrowser.Providers.Music
|
||||
protected override bool EnableUpdatingGenresFromChildren => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item)
|
||||
protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item)
|
||||
{
|
||||
return item.IsAccessedByName
|
||||
? item.GetTaggedItems(new InternalItemsQuery
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace MediaBrowser.Providers.Playlists
|
||||
protected override bool EnableUpdatingStudiosFromChildren => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IList<BaseItem> GetChildrenForMetadataUpdates(Playlist item)
|
||||
protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Playlist item)
|
||||
=> item.GetLinkedChildren();
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -148,21 +148,19 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
item.Overview = (overview ?? string.Empty).StripHtml();
|
||||
}
|
||||
|
||||
internal Task EnsureInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken)
|
||||
internal async Task EnsureInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken)
|
||||
{
|
||||
var xmlPath = GetAlbumInfoPath(_config.ApplicationPaths, musicBrainzReleaseGroupId);
|
||||
|
||||
var fileInfo = _fileSystem.GetFileSystemInfo(xmlPath);
|
||||
|
||||
if (fileInfo.Exists)
|
||||
if (fileInfo.Exists
|
||||
&& (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
|
||||
{
|
||||
if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return DownloadInfo(musicBrainzReleaseGroupId, cancellationToken);
|
||||
await DownloadInfo(musicBrainzReleaseGroupId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal async Task DownloadInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken)
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
item.Overview = (overview ?? string.Empty).StripHtml();
|
||||
}
|
||||
|
||||
internal Task EnsureArtistInfo(string musicBrainzId, CancellationToken cancellationToken)
|
||||
internal async Task EnsureArtistInfo(string musicBrainzId, CancellationToken cancellationToken)
|
||||
{
|
||||
var xmlPath = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
|
||||
|
||||
@@ -140,10 +140,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
if (fileInfo.Exists
|
||||
&& (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
return DownloadArtistInfo(musicBrainzId, cancellationToken);
|
||||
await DownloadArtistInfo(musicBrainzId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal async Task DownloadArtistInfo(string musicBrainzId, CancellationToken cancellationToken)
|
||||
|
||||
@@ -101,11 +101,11 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}/images/{1}/{2}.jpg", GetRepositoryUrl(), image, filename);
|
||||
}
|
||||
|
||||
private Task EnsureThumbsList(string file, CancellationToken cancellationToken)
|
||||
private async Task EnsureThumbsList(string file, CancellationToken cancellationToken)
|
||||
{
|
||||
string url = string.Format(CultureInfo.InvariantCulture, "{0}/thumbs.txt", GetRepositoryUrl());
|
||||
|
||||
return EnsureList(url, file, _fileSystem, cancellationToken);
|
||||
await EnsureList(url, file, _fileSystem, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -199,7 +199,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
|
||||
};
|
||||
|
||||
movie.SetProviderId(MetadataProvider.Tmdb, tmdbId);
|
||||
movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
|
||||
movie.TrySetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
|
||||
if (movieResult.BelongsToCollection is not null)
|
||||
{
|
||||
movie.SetProviderId(MetadataProvider.TmdbCollection, movieResult.BelongsToCollection.Id.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
@@ -12,8 +10,19 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Providers.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Service to manage episode metadata.
|
||||
/// </summary>
|
||||
public class EpisodeMetadataService : MetadataService<Episode, EpisodeInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EpisodeMetadataService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
public EpisodeMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<EpisodeMetadataService> logger,
|
||||
@@ -94,6 +103,11 @@ namespace MediaBrowser.Providers.TV
|
||||
{
|
||||
targetItem.IndexNumberEnd = sourceItem.IndexNumberEnd;
|
||||
}
|
||||
|
||||
if (replaceData || !targetItem.ParentIndexNumber.HasValue)
|
||||
{
|
||||
targetItem.ParentIndexNumber = sourceItem.ParentIndexNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -15,8 +13,19 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Providers.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Service to manage season metadata.
|
||||
/// </summary>
|
||||
public class SeasonMetadataService : MetadataService<Season, SeasonInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SeasonMetadataService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
public SeasonMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<SeasonMetadataService> logger,
|
||||
@@ -71,11 +80,11 @@ namespace MediaBrowser.Providers.TV
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IList<BaseItem> GetChildrenForMetadataUpdates(Season item)
|
||||
protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Season item)
|
||||
=> item.GetEpisodes();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
|
||||
protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
|
||||
{
|
||||
var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType);
|
||||
|
||||
@@ -87,7 +96,7 @@ namespace MediaBrowser.Providers.TV
|
||||
return updateType;
|
||||
}
|
||||
|
||||
private ItemUpdateType SaveIsVirtualItem(Season item, IList<BaseItem> episodes)
|
||||
private ItemUpdateType SaveIsVirtualItem(Season item, IReadOnlyList<BaseItem> episodes)
|
||||
{
|
||||
var isVirtualItem = item.LocationType == LocationType.Virtual && (episodes.Count == 0 || episodes.All(i => i.LocationType == LocationType.Virtual));
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
@@ -20,10 +18,22 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Providers.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Service to manage series metadata.
|
||||
/// </summary>
|
||||
public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
|
||||
{
|
||||
private readonly ILocalizationManager _localizationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SeriesMetadataService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
public SeriesMetadataService(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<SeriesMetadataService> logger,
|
||||
@@ -36,6 +46,7 @@ namespace MediaBrowser.Providers.TV
|
||||
_localizationManager = localizationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
if (item is Series series)
|
||||
@@ -129,38 +140,39 @@ namespace MediaBrowser.Providers.TV
|
||||
|
||||
private void RemoveObsoleteEpisodes(Series series)
|
||||
{
|
||||
var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList();
|
||||
var numberOfEpisodes = episodes.Count;
|
||||
// TODO: O(n^2), but can it be done faster without overcomplicating it?
|
||||
for (var i = 0; i < numberOfEpisodes; i++)
|
||||
var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true)
|
||||
.OfType<Episode>()
|
||||
.GroupBy(e => e.ParentIndexNumber)
|
||||
.ToList();
|
||||
|
||||
foreach (var seasonEpisodes in episodesBySeason)
|
||||
{
|
||||
var currentEpisode = episodes[i];
|
||||
// The outer loop only examines virtual episodes
|
||||
if (!currentEpisode.IsVirtualItem)
|
||||
List<Episode> nonPhysicalEpisodes = [];
|
||||
List<Episode> physicalEpisodes = [];
|
||||
foreach (var episode in seasonEpisodes)
|
||||
{
|
||||
continue;
|
||||
if (episode.IsVirtualItem || episode.IsMissingEpisode)
|
||||
{
|
||||
nonPhysicalEpisodes.Add(episode);
|
||||
continue;
|
||||
}
|
||||
|
||||
physicalEpisodes.Add(episode);
|
||||
}
|
||||
|
||||
// Virtual episodes without an episode number are practically orphaned and should be deleted
|
||||
if (!currentEpisode.IndexNumber.HasValue)
|
||||
// Only consider non-physical episodes
|
||||
foreach (var episode in nonPhysicalEpisodes)
|
||||
{
|
||||
DeleteEpisode(currentEpisode);
|
||||
continue;
|
||||
}
|
||||
// Episodes without an episode number are practically orphaned and should be deleted
|
||||
// Episodes with a physical equivalent should be deleted (they are no longer missing)
|
||||
var shouldKeep = episode.IndexNumber.HasValue && !physicalEpisodes.Any(e => e.ContainsEpisodeNumber(episode.IndexNumber.Value));
|
||||
|
||||
for (var j = i + 1; j < numberOfEpisodes; j++)
|
||||
{
|
||||
var comparisonEpisode = episodes[j];
|
||||
// The inner loop is only for "physical" episodes
|
||||
if (comparisonEpisode.IsVirtualItem
|
||||
|| currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber
|
||||
|| !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value))
|
||||
if (shouldKeep)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DeleteEpisode(currentEpisode);
|
||||
break;
|
||||
DeleteEpisode(episode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public class TrickplayImagesTask : IScheduledTask
|
||||
{
|
||||
new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfo.TriggerDaily,
|
||||
Type = TaskTriggerInfoType.DailyTrigger,
|
||||
TimeOfDayTicks = TimeSpan.FromHours(3).Ticks
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,8 +18,6 @@ namespace MediaBrowser.Providers.Trickplay;
|
||||
/// </summary>
|
||||
public class TrickplayMoveImagesTask : IScheduledTask
|
||||
{
|
||||
private const int QueryPageLimit = 100;
|
||||
|
||||
private readonly ILogger<TrickplayMoveImagesTask> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
@@ -62,32 +60,46 @@ public class TrickplayMoveImagesTask : IScheduledTask
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var trickplayItems = await _trickplayManager.GetTrickplayItemsAsync().ConfigureAwait(false);
|
||||
var query = new InternalItemsQuery
|
||||
const int Limit = 100;
|
||||
int itemCount = 0, offset = 0, previousCount;
|
||||
|
||||
// This count may not be accurate, but just get something to show progress on the dashboard.
|
||||
var totalVideoCount = _libraryManager.GetCount(new InternalItemsQuery
|
||||
{
|
||||
MediaTypes = [MediaType.Video],
|
||||
SourceTypes = [SourceType.Library],
|
||||
IsVirtualItem = false,
|
||||
IsFolder = false,
|
||||
Recursive = true,
|
||||
Limit = QueryPageLimit
|
||||
Recursive = true
|
||||
});
|
||||
|
||||
var trickplayQuery = new InternalItemsQuery
|
||||
{
|
||||
MediaTypes = [MediaType.Video],
|
||||
SourceTypes = [SourceType.Library],
|
||||
IsVirtualItem = false,
|
||||
IsFolder = false
|
||||
};
|
||||
|
||||
var numberOfVideos = _libraryManager.GetCount(query);
|
||||
|
||||
var startIndex = 0;
|
||||
var numComplete = 0;
|
||||
|
||||
while (startIndex < numberOfVideos)
|
||||
do
|
||||
{
|
||||
query.StartIndex = startIndex;
|
||||
var videos = _libraryManager.GetItemList(query).OfType<Video>().ToList();
|
||||
videos.RemoveAll(i => !trickplayItems.Contains(i.Id));
|
||||
var trickplayInfos = await _trickplayManager.GetTrickplayItemsAsync(Limit, offset).ConfigureAwait(false);
|
||||
previousCount = trickplayInfos.Count;
|
||||
offset += Limit;
|
||||
|
||||
foreach (var video in videos)
|
||||
trickplayQuery.ItemIds = trickplayInfos.Select(i => i.ItemId).Distinct().ToArray();
|
||||
var items = _libraryManager.GetItemList(trickplayQuery);
|
||||
foreach (var trickplayInfo in trickplayInfos)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var video = items.OfType<Video>().FirstOrDefault(i => i.Id.Equals(trickplayInfo.ItemId));
|
||||
if (video is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
itemCount++;
|
||||
try
|
||||
{
|
||||
var libraryOptions = _libraryManager.GetLibraryOptions(video);
|
||||
@@ -97,13 +109,10 @@ public class TrickplayMoveImagesTask : IScheduledTask
|
||||
{
|
||||
_logger.LogError(ex, "Error moving trickplay files for {ItemName}", video.Name);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
progress.Report(100d * numComplete / numberOfVideos);
|
||||
}
|
||||
|
||||
startIndex += QueryPageLimit;
|
||||
}
|
||||
progress.Report(100d * itemCount / totalVideoCount);
|
||||
} while (previousCount == Limit);
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user