Browse Source

duration analysis on files with missing duration metadata (#683)

* first pass

* analyze zero-duration files
pull/684/head
Jason Dove 3 years ago committed by GitHub
parent
commit
52a8b7db81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      CHANGELOG.md
  2. 16
      ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs
  3. 16
      ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs
  4. 4
      ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs
  5. 16
      ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs
  6. 1
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  7. 31
      ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs
  8. 7
      ErsatzTV.Core/Emby/EmbyMovieLibraryScanner.cs
  9. 18
      ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs
  10. 1
      ErsatzTV.Core/Interfaces/Emby/IEmbyMovieLibraryScanner.cs
  11. 1
      ErsatzTV.Core/Interfaces/Emby/IEmbyTelevisionLibraryScanner.cs
  12. 1
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinMovieLibraryScanner.cs
  13. 1
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinTelevisionLibraryScanner.cs
  14. 9
      ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs
  15. 1
      ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs
  16. 1
      ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs
  17. 1
      ErsatzTV.Core/Interfaces/Metadata/IOtherVideoFolderScanner.cs
  18. 1
      ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs
  19. 1
      ErsatzTV.Core/Interfaces/Plex/IPlexMovieLibraryScanner.cs
  20. 1
      ErsatzTV.Core/Interfaces/Plex/IPlexTelevisionLibraryScanner.cs
  21. 7
      ErsatzTV.Core/Jellyfin/JellyfinMovieLibraryScanner.cs
  22. 18
      ErsatzTV.Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs
  23. 3
      ErsatzTV.Core/Metadata/LocalFolderScanner.cs
  24. 86
      ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs
  25. 3
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  26. 5
      ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs
  27. 3
      ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs
  28. 2
      ErsatzTV.Core/Metadata/SongFolderScanner.cs
  29. 8
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  30. 7
      ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs
  31. 17
      ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs
  32. 3886
      ErsatzTV.Infrastructure/Migrations/20220308033129_Analyze_ZeroDurationFiles.Designer.cs
  33. 50
      ErsatzTV.Infrastructure/Migrations/20220308033129_Analyze_ZeroDurationFiles.cs

3
CHANGELOG.md

@ -7,6 +7,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed ### Fixed
- Fix `HLS Direct` streaming mode - Fix `HLS Direct` streaming mode
### Added
- Perform additional duration analysis on files with missing duration metadata
## [0.4.3-alpha] - 2022-03-05 ## [0.4.3-alpha] - 2022-03-05
### Fixed ### Fixed
- Fix song sorting with `Chronological` and `Shuffle In Order` playback orders - Fix song sorting with `Chronological` and `Shuffle In Order` playback orders

16
ErsatzTV.Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs

@ -70,6 +70,7 @@ public class SynchronizeEmbyLibraryByIdHandler :
parameters.ConnectionParameters.ActiveConnection.Address, parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey, parameters.ConnectionParameters.ApiKey,
parameters.Library, parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath); parameters.FFprobePath);
break; break;
case LibraryMediaKind.Shows: case LibraryMediaKind.Shows:
@ -77,6 +78,7 @@ public class SynchronizeEmbyLibraryByIdHandler :
parameters.ConnectionParameters.ActiveConnection.Address, parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey, parameters.ConnectionParameters.ApiKey,
parameters.Library, parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath); parameters.FFprobePath);
break; break;
} }
@ -98,13 +100,15 @@ public class SynchronizeEmbyLibraryByIdHandler :
private async Task<Validation<BaseError, RequestParameters>> Validate( private async Task<Validation<BaseError, RequestParameters>> Validate(
ISynchronizeEmbyLibraryById request) => ISynchronizeEmbyLibraryById request) =>
(await ValidateConnection(request), await EmbyLibraryMustExist(request), (await ValidateConnection(request), await EmbyLibraryMustExist(request),
await ValidateLibraryRefreshInterval(), await ValidateFFprobePath()) await ValidateLibraryRefreshInterval(), await ValidateFFmpegPath(), await ValidateFFprobePath())
.Apply( .Apply(
(connectionParameters, embyLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters( (connectionParameters, embyLibrary, libraryRefreshInterval, ffmpegPath, ffprobePath) =>
new RequestParameters(
connectionParameters, connectionParameters,
embyLibrary, embyLibrary,
request.ForceScan, request.ForceScan,
libraryRefreshInterval, libraryRefreshInterval,
ffmpegPath,
ffprobePath ffprobePath
)); ));
@ -149,6 +153,13 @@ public class SynchronizeEmbyLibraryByIdHandler :
.FilterT(lri => lri > 0) .FilterT(lri => lri > 0)
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid")); .Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
private Task<Validation<BaseError, string>> ValidateFFmpegPath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath)
.FilterT(File.Exists)
.Map(
ffmpegPath =>
ffmpegPath.ToValidation<BaseError>("FFmpeg path does not exist on the file system"));
private Task<Validation<BaseError, string>> ValidateFFprobePath() => private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath) _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists) .FilterT(File.Exists)
@ -161,6 +172,7 @@ public class SynchronizeEmbyLibraryByIdHandler :
EmbyLibrary Library, EmbyLibrary Library,
bool ForceScan, bool ForceScan,
int LibraryRefreshInterval, int LibraryRefreshInterval,
string FFmpegPath,
string FFprobePath); string FFprobePath);
private record ConnectionParameters( private record ConnectionParameters(

16
ErsatzTV.Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs

@ -70,6 +70,7 @@ public class SynchronizeJellyfinLibraryByIdHandler :
parameters.ConnectionParameters.ActiveConnection.Address, parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey, parameters.ConnectionParameters.ApiKey,
parameters.Library, parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath); parameters.FFprobePath);
break; break;
case LibraryMediaKind.Shows: case LibraryMediaKind.Shows:
@ -77,6 +78,7 @@ public class SynchronizeJellyfinLibraryByIdHandler :
parameters.ConnectionParameters.ActiveConnection.Address, parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey, parameters.ConnectionParameters.ApiKey,
parameters.Library, parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath); parameters.FFprobePath);
break; break;
} }
@ -98,13 +100,15 @@ public class SynchronizeJellyfinLibraryByIdHandler :
private async Task<Validation<BaseError, RequestParameters>> Validate( private async Task<Validation<BaseError, RequestParameters>> Validate(
ISynchronizeJellyfinLibraryById request) => ISynchronizeJellyfinLibraryById request) =>
(await ValidateConnection(request), await JellyfinLibraryMustExist(request), (await ValidateConnection(request), await JellyfinLibraryMustExist(request),
await ValidateLibraryRefreshInterval(), await ValidateFFprobePath()) await ValidateLibraryRefreshInterval(), await ValidateFFmpegPath(), await ValidateFFprobePath())
.Apply( .Apply(
(connectionParameters, jellyfinLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters( (connectionParameters, jellyfinLibrary, libraryRefreshInterval, ffmpegPath, ffprobePath) =>
new RequestParameters(
connectionParameters, connectionParameters,
jellyfinLibrary, jellyfinLibrary,
request.ForceScan, request.ForceScan,
libraryRefreshInterval, libraryRefreshInterval,
ffmpegPath,
ffprobePath ffprobePath
)); ));
@ -149,6 +153,13 @@ public class SynchronizeJellyfinLibraryByIdHandler :
.FilterT(lri => lri > 0) .FilterT(lri => lri > 0)
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid")); .Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
private Task<Validation<BaseError, string>> ValidateFFmpegPath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath)
.FilterT(File.Exists)
.Map(
ffmpegPath =>
ffmpegPath.ToValidation<BaseError>("FFmpeg path does not exist on the file system"));
private Task<Validation<BaseError, string>> ValidateFFprobePath() => private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath) _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists) .FilterT(File.Exists)
@ -161,6 +172,7 @@ public class SynchronizeJellyfinLibraryByIdHandler :
JellyfinLibrary Library, JellyfinLibrary Library,
bool ForceScan, bool ForceScan,
int LibraryRefreshInterval, int LibraryRefreshInterval,
string FFmpegPath,
string FFprobePath); string FFprobePath);
private record ConnectionParameters( private record ConnectionParameters(

4
ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs

@ -89,6 +89,7 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei
case LibraryMediaKind.Movies: case LibraryMediaKind.Movies:
await _movieFolderScanner.ScanFolder( await _movieFolderScanner.ScanFolder(
libraryPath, libraryPath,
ffmpegPath,
ffprobePath, ffprobePath,
progressMin, progressMin,
progressMax); progressMax);
@ -96,6 +97,7 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei
case LibraryMediaKind.Shows: case LibraryMediaKind.Shows:
await _televisionFolderScanner.ScanFolder( await _televisionFolderScanner.ScanFolder(
libraryPath, libraryPath,
ffmpegPath,
ffprobePath, ffprobePath,
progressMin, progressMin,
progressMax); progressMax);
@ -103,6 +105,7 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei
case LibraryMediaKind.MusicVideos: case LibraryMediaKind.MusicVideos:
await _musicVideoFolderScanner.ScanFolder( await _musicVideoFolderScanner.ScanFolder(
libraryPath, libraryPath,
ffmpegPath,
ffprobePath, ffprobePath,
progressMin, progressMin,
progressMax); progressMax);
@ -110,6 +113,7 @@ public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Ei
case LibraryMediaKind.OtherVideos: case LibraryMediaKind.OtherVideos:
await _otherVideoFolderScanner.ScanFolder( await _otherVideoFolderScanner.ScanFolder(
libraryPath, libraryPath,
ffmpegPath,
ffprobePath, ffprobePath,
progressMin, progressMin,
progressMax); progressMax);

16
ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs

@ -68,6 +68,7 @@ public class
parameters.ConnectionParameters.ActiveConnection, parameters.ConnectionParameters.ActiveConnection,
parameters.ConnectionParameters.PlexServerAuthToken, parameters.ConnectionParameters.PlexServerAuthToken,
parameters.Library, parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath); parameters.FFprobePath);
break; break;
case LibraryMediaKind.Shows: case LibraryMediaKind.Shows:
@ -75,6 +76,7 @@ public class
parameters.ConnectionParameters.ActiveConnection, parameters.ConnectionParameters.ActiveConnection,
parameters.ConnectionParameters.PlexServerAuthToken, parameters.ConnectionParameters.PlexServerAuthToken,
parameters.Library, parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath); parameters.FFprobePath);
break; break;
} }
@ -95,13 +97,15 @@ public class
private async Task<Validation<BaseError, RequestParameters>> Validate(ISynchronizePlexLibraryById request) => private async Task<Validation<BaseError, RequestParameters>> Validate(ISynchronizePlexLibraryById request) =>
(await ValidateConnection(request), await PlexLibraryMustExist(request), (await ValidateConnection(request), await PlexLibraryMustExist(request),
await ValidateLibraryRefreshInterval(), await ValidateFFprobePath()) await ValidateLibraryRefreshInterval(), await ValidateFFmpegPath(), await ValidateFFprobePath())
.Apply( .Apply(
(connectionParameters, plexLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters( (connectionParameters, plexLibrary, libraryRefreshInterval, ffmpegPath, ffprobePath) =>
new RequestParameters(
connectionParameters, connectionParameters,
plexLibrary, plexLibrary,
request.ForceScan, request.ForceScan,
libraryRefreshInterval, libraryRefreshInterval,
ffmpegPath,
ffprobePath ffprobePath
)); ));
@ -146,6 +150,13 @@ public class
.FilterT(lri => lri > 0) .FilterT(lri => lri > 0)
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid")); .Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
private Task<Validation<BaseError, string>> ValidateFFmpegPath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath)
.FilterT(File.Exists)
.Map(
ffmpegPath =>
ffmpegPath.ToValidation<BaseError>("FFmpeg path does not exist on the file system"));
private Task<Validation<BaseError, string>> ValidateFFprobePath() => private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath) _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists) .FilterT(File.Exists)
@ -158,6 +169,7 @@ public class
PlexLibrary Library, PlexLibrary Library,
bool ForceScan, bool ForceScan,
int LibraryRefreshInterval, int LibraryRefreshInterval,
string FFmpegPath,
string FFprobePath); string FFprobePath);
private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection) private record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection)

1
ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs

@ -282,6 +282,7 @@ public class TranscodingTests
LoggerFactory.CreateLogger<LocalStatisticsProvider>()); LoggerFactory.CreateLogger<LocalStatisticsProvider>());
await localStatisticsProvider.RefreshStatistics( await localStatisticsProvider.RefreshStatistics(
ExecutableName("ffmpeg"),
ExecutableName("ffprobe"), ExecutableName("ffprobe"),
new Movie new Movie
{ {

31
ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs

@ -27,6 +27,10 @@ public class MovieFolderScannerTests
? @"C:\Movies" ? @"C:\Movies"
: "/movies"; : "/movies";
private static readonly string FFmpegPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? @"C:\bin\ffmpeg.exe"
: "/bin/ffmpeg";
private static readonly string FFprobePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) private static readonly string FFprobePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? @"C:\bin\ffprobe.exe" ? @"C:\bin\ffprobe.exe"
: "/bin/ffprobe"; : "/bin/ffprobe";
@ -52,8 +56,9 @@ public class MovieFolderScannerTests
_localStatisticsProvider = new Mock<ILocalStatisticsProvider>(); _localStatisticsProvider = new Mock<ILocalStatisticsProvider>();
_localMetadataProvider = new Mock<ILocalMetadataProvider>(); _localMetadataProvider = new Mock<ILocalMetadataProvider>();
_localStatisticsProvider.Setup(x => x.RefreshStatistics(It.IsAny<string>(), It.IsAny<MediaItem>())) _localStatisticsProvider.Setup(
.Returns<string, MediaItem>((_, _) => Right<BaseError, bool>(true).AsTask()); x => x.RefreshStatistics(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MediaItem>()))
.Returns<string, string, MediaItem>((_, _, _) => Right<BaseError, bool>(true).AsTask());
// fallback metadata adds metadata to a movie, so we need to replicate that here // fallback metadata adds metadata to a movie, so we need to replicate that here
_localMetadataProvider.Setup(x => x.RefreshFallbackMetadata(It.IsAny<Movie>())) _localMetadataProvider.Setup(x => x.RefreshFallbackMetadata(It.IsAny<Movie>()))
@ -90,6 +95,7 @@ public class MovieFolderScannerTests
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFmpegPath,
FFprobePath, FFprobePath,
0, 0,
1); 1);
@ -101,6 +107,7 @@ public class MovieFolderScannerTests
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics( x => x.RefreshStatistics(
FFmpegPath,
FFprobePath, FFprobePath,
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
Times.Once); Times.Once);
@ -131,6 +138,7 @@ public class MovieFolderScannerTests
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFmpegPath,
FFprobePath, FFprobePath,
0, 0,
1); 1);
@ -142,6 +150,7 @@ public class MovieFolderScannerTests
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics( x => x.RefreshStatistics(
FFmpegPath,
FFprobePath, FFprobePath,
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
Times.Once); Times.Once);
@ -173,6 +182,7 @@ public class MovieFolderScannerTests
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFmpegPath,
FFprobePath, FFprobePath,
0, 0,
1); 1);
@ -184,6 +194,7 @@ public class MovieFolderScannerTests
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics( x => x.RefreshStatistics(
FFmpegPath,
FFprobePath, FFprobePath,
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
Times.Once); Times.Once);
@ -219,6 +230,7 @@ public class MovieFolderScannerTests
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFmpegPath,
FFprobePath, FFprobePath,
0, 0,
1); 1);
@ -230,6 +242,7 @@ public class MovieFolderScannerTests
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics( x => x.RefreshStatistics(
FFmpegPath,
FFprobePath, FFprobePath,
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
Times.Once); Times.Once);
@ -268,6 +281,7 @@ public class MovieFolderScannerTests
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFmpegPath,
FFprobePath, FFprobePath,
0, 0,
1); 1);
@ -279,6 +293,7 @@ public class MovieFolderScannerTests
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics( x => x.RefreshStatistics(
FFmpegPath,
FFprobePath, FFprobePath,
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
Times.Once); Times.Once);
@ -317,6 +332,7 @@ public class MovieFolderScannerTests
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFmpegPath,
FFprobePath, FFprobePath,
0, 0,
1); 1);
@ -328,6 +344,7 @@ public class MovieFolderScannerTests
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics( x => x.RefreshStatistics(
FFmpegPath,
FFprobePath, FFprobePath,
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
Times.Once); Times.Once);
@ -365,6 +382,7 @@ public class MovieFolderScannerTests
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFmpegPath,
FFprobePath, FFprobePath,
0, 0,
1); 1);
@ -376,6 +394,7 @@ public class MovieFolderScannerTests
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics( x => x.RefreshStatistics(
FFmpegPath,
FFprobePath, FFprobePath,
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
Times.Once); Times.Once);
@ -407,6 +426,7 @@ public class MovieFolderScannerTests
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFmpegPath,
FFprobePath, FFprobePath,
0, 0,
1); 1);
@ -418,6 +438,7 @@ public class MovieFolderScannerTests
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics( x => x.RefreshStatistics(
FFmpegPath,
FFprobePath, FFprobePath,
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
Times.Once); Times.Once);
@ -451,6 +472,7 @@ public class MovieFolderScannerTests
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFmpegPath,
FFprobePath, FFprobePath,
0, 0,
1); 1);
@ -462,6 +484,7 @@ public class MovieFolderScannerTests
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics( x => x.RefreshStatistics(
FFmpegPath,
FFprobePath, FFprobePath,
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
Times.Once); Times.Once);
@ -489,6 +512,7 @@ public class MovieFolderScannerTests
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFmpegPath,
FFprobePath, FFprobePath,
0, 0,
1); 1);
@ -500,6 +524,7 @@ public class MovieFolderScannerTests
_localStatisticsProvider.Verify( _localStatisticsProvider.Verify(
x => x.RefreshStatistics( x => x.RefreshStatistics(
FFmpegPath,
FFprobePath, FFprobePath,
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)), It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
Times.Once); Times.Once);
@ -532,6 +557,7 @@ public class MovieFolderScannerTests
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFmpegPath,
FFprobePath, FFprobePath,
0, 0,
1); 1);
@ -561,6 +587,7 @@ public class MovieFolderScannerTests
Either<BaseError, Unit> result = await service.ScanFolder( Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath, libraryPath,
FFmpegPath,
FFprobePath, FFprobePath,
0, 0,
1); 1);

7
ErsatzTV.Core/Emby/EmbyMovieLibraryScanner.cs

@ -51,6 +51,7 @@ public class EmbyMovieLibraryScanner : IEmbyMovieLibraryScanner
string address, string address,
string apiKey, string apiKey,
EmbyLibrary library, EmbyLibrary library,
string ffmpegPath,
string ffprobePath) string ffprobePath)
{ {
List<EmbyItemEtag> existingMovies = await _movieRepository.GetExistingEmbyMovies(library); List<EmbyItemEtag> existingMovies = await _movieRepository.GetExistingEmbyMovies(library);
@ -172,7 +173,11 @@ public class EmbyMovieLibraryScanner : IEmbyMovieLibraryScanner
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath); _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath);
Either<BaseError, bool> refreshResult = Either<BaseError, bool> refreshResult =
await _localStatisticsProvider.RefreshStatistics(ffprobePath, incomingMovie, localPath); await _localStatisticsProvider.RefreshStatistics(
ffmpegPath,
ffprobePath,
incomingMovie,
localPath);
await refreshResult.Match( await refreshResult.Match(
async _ => async _ =>

18
ErsatzTV.Core/Emby/EmbyTelevisionLibraryScanner.cs

@ -51,6 +51,7 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
string address, string address,
string apiKey, string apiKey,
EmbyLibrary library, EmbyLibrary library,
string ffmpegPath,
string ffprobePath) string ffprobePath)
{ {
List<EmbyItemEtag> existingShows = await _televisionRepository.GetExistingShows(library); List<EmbyItemEtag> existingShows = await _televisionRepository.GetExistingShows(library);
@ -70,7 +71,15 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
await maybeShows.Match( await maybeShows.Match(
async shows => async shows =>
{ {
await ProcessShows(address, apiKey, library, ffprobePath, pathReplacements, existingShows, shows); await ProcessShows(
address,
apiKey,
library,
ffmpegPath,
ffprobePath,
pathReplacements,
existingShows,
shows);
var incomingShowIds = shows.Map(s => s.ItemId).ToList(); var incomingShowIds = shows.Map(s => s.ItemId).ToList();
var showIds = existingShows var showIds = existingShows
@ -104,6 +113,7 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
string address, string address,
string apiKey, string apiKey,
EmbyLibrary library, EmbyLibrary library,
string ffmpegPath,
string ffprobePath, string ffprobePath,
List<EmbyPathReplacement> pathReplacements, List<EmbyPathReplacement> pathReplacements,
List<EmbyItemEtag> existingShows, List<EmbyItemEtag> existingShows,
@ -167,6 +177,7 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
address, address,
apiKey, apiKey,
library, library,
ffmpegPath,
ffprobePath, ffprobePath,
pathReplacements, pathReplacements,
incoming, incoming,
@ -196,6 +207,7 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
string address, string address,
string apiKey, string apiKey,
EmbyLibrary library, EmbyLibrary library,
string ffmpegPath,
string ffprobePath, string ffprobePath,
List<EmbyPathReplacement> pathReplacements, List<EmbyPathReplacement> pathReplacements,
EmbyShow show, EmbyShow show,
@ -286,6 +298,7 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
show.ShowMetadata.Head().Title, show.ShowMetadata.Head().Title,
incoming.SeasonMetadata.Head().Title, incoming.SeasonMetadata.Head().Title,
library, library,
ffmpegPath,
ffprobePath, ffprobePath,
pathReplacements, pathReplacements,
incoming, incoming,
@ -318,6 +331,7 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
string showName, string showName,
string seasonName, string seasonName,
EmbyLibrary library, EmbyLibrary library,
string ffmpegPath,
string ffprobePath, string ffprobePath,
List<EmbyPathReplacement> pathReplacements, List<EmbyPathReplacement> pathReplacements,
EmbySeason season, EmbySeason season,
@ -406,7 +420,7 @@ public class EmbyTelevisionLibraryScanner : IEmbyTelevisionLibraryScanner
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath); _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath);
Either<BaseError, bool> refreshResult = Either<BaseError, bool> refreshResult =
await _localStatisticsProvider.RefreshStatistics(ffprobePath, incomingEpisode, localPath); await _localStatisticsProvider.RefreshStatistics(ffmpegPath, ffprobePath, incomingEpisode, localPath);
refreshResult.Match( refreshResult.Match(
_ => { }, _ => { },

1
ErsatzTV.Core/Interfaces/Emby/IEmbyMovieLibraryScanner.cs

@ -8,5 +8,6 @@ public interface IEmbyMovieLibraryScanner
string address, string address,
string apiKey, string apiKey,
EmbyLibrary library, EmbyLibrary library,
string ffmpegPath,
string ffprobePath); string ffprobePath);
} }

1
ErsatzTV.Core/Interfaces/Emby/IEmbyTelevisionLibraryScanner.cs

@ -8,5 +8,6 @@ public interface IEmbyTelevisionLibraryScanner
string address, string address,
string apiKey, string apiKey,
EmbyLibrary library, EmbyLibrary library,
string ffmpegPath,
string ffprobePath); string ffprobePath);
} }

1
ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinMovieLibraryScanner.cs

@ -8,5 +8,6 @@ public interface IJellyfinMovieLibraryScanner
string address, string address,
string apiKey, string apiKey,
JellyfinLibrary library, JellyfinLibrary library,
string ffmpegPath,
string ffprobePath); string ffprobePath);
} }

1
ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinTelevisionLibraryScanner.cs

@ -8,5 +8,6 @@ public interface IJellyfinTelevisionLibraryScanner
string address, string address,
string apiKey, string apiKey,
JellyfinLibrary library, JellyfinLibrary library,
string ffmpegPath,
string ffprobePath); string ffprobePath);
} }

9
ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs

@ -4,8 +4,13 @@ namespace ErsatzTV.Core.Interfaces.Metadata;
public interface ILocalStatisticsProvider public interface ILocalStatisticsProvider
{ {
Task<Either<BaseError, bool>> RefreshStatistics(string ffprobePath, MediaItem mediaItem); Task<Either<BaseError, bool>> RefreshStatistics(string ffmpegPath, string ffprobePath, MediaItem mediaItem);
Task<Either<BaseError, bool>> RefreshStatistics(string ffprobePath, MediaItem mediaItem, string mediaItemPath);
Task<Either<BaseError, bool>> RefreshStatistics(
string ffmpegPath,
string ffprobePath,
MediaItem mediaItem,
string mediaItemPath);
Task<Either<BaseError, Dictionary<string, string>>> GetFormatTags(string ffprobePath, MediaItem mediaItem); Task<Either<BaseError, Dictionary<string, string>>> GetFormatTags(string ffprobePath, MediaItem mediaItem);
} }

1
ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs

@ -6,6 +6,7 @@ public interface IMovieFolderScanner
{ {
Task<Either<BaseError, Unit>> ScanFolder( Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffmpegPath,
string ffprobePath, string ffprobePath,
decimal progressMin, decimal progressMin,
decimal progressMax); decimal progressMax);

1
ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs

@ -6,6 +6,7 @@ public interface IMusicVideoFolderScanner
{ {
Task<Either<BaseError, Unit>> ScanFolder( Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffmpegPath,
string ffprobePath, string ffprobePath,
decimal progressMin, decimal progressMin,
decimal progressMax); decimal progressMax);

1
ErsatzTV.Core/Interfaces/Metadata/IOtherVideoFolderScanner.cs

@ -6,6 +6,7 @@ public interface IOtherVideoFolderScanner
{ {
Task<Either<BaseError, Unit>> ScanFolder( Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffmpegPath,
string ffprobePath, string ffprobePath,
decimal progressMin, decimal progressMin,
decimal progressMax); decimal progressMax);

1
ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs

@ -6,6 +6,7 @@ public interface ITelevisionFolderScanner
{ {
Task<Either<BaseError, Unit>> ScanFolder( Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffmpegPath,
string ffprobePath, string ffprobePath,
decimal progressMin, decimal progressMin,
decimal progressMax); decimal progressMax);

1
ErsatzTV.Core/Interfaces/Plex/IPlexMovieLibraryScanner.cs

@ -9,5 +9,6 @@ public interface IPlexMovieLibraryScanner
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
PlexLibrary library, PlexLibrary library,
string ffmpegPath,
string ffprobePath); string ffprobePath);
} }

1
ErsatzTV.Core/Interfaces/Plex/IPlexTelevisionLibraryScanner.cs

@ -9,5 +9,6 @@ public interface IPlexTelevisionLibraryScanner
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
PlexLibrary library, PlexLibrary library,
string ffmpegPath,
string ffprobePath); string ffprobePath);
} }

7
ErsatzTV.Core/Jellyfin/JellyfinMovieLibraryScanner.cs

@ -51,6 +51,7 @@ public class JellyfinMovieLibraryScanner : IJellyfinMovieLibraryScanner
string address, string address,
string apiKey, string apiKey,
JellyfinLibrary library, JellyfinLibrary library,
string ffmpegPath,
string ffprobePath) string ffprobePath)
{ {
List<JellyfinItemEtag> existingMovies = await _movieRepository.GetExistingJellyfinMovies(library); List<JellyfinItemEtag> existingMovies = await _movieRepository.GetExistingJellyfinMovies(library);
@ -172,7 +173,11 @@ public class JellyfinMovieLibraryScanner : IJellyfinMovieLibraryScanner
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath); _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath);
Either<BaseError, bool> refreshResult = Either<BaseError, bool> refreshResult =
await _localStatisticsProvider.RefreshStatistics(ffprobePath, incomingMovie, localPath); await _localStatisticsProvider.RefreshStatistics(
ffmpegPath,
ffprobePath,
incomingMovie,
localPath);
await refreshResult.Match( await refreshResult.Match(
async _ => async _ =>

18
ErsatzTV.Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs

@ -51,6 +51,7 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne
string address, string address,
string apiKey, string apiKey,
JellyfinLibrary library, JellyfinLibrary library,
string ffmpegPath,
string ffprobePath) string ffprobePath)
{ {
List<JellyfinItemEtag> existingShows = await _televisionRepository.GetExistingShows(library); List<JellyfinItemEtag> existingShows = await _televisionRepository.GetExistingShows(library);
@ -70,7 +71,15 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne
await maybeShows.Match( await maybeShows.Match(
async shows => async shows =>
{ {
await ProcessShows(address, apiKey, library, ffprobePath, pathReplacements, existingShows, shows); await ProcessShows(
address,
apiKey,
library,
ffmpegPath,
ffprobePath,
pathReplacements,
existingShows,
shows);
var incomingShowIds = shows.Map(s => s.ItemId).ToList(); var incomingShowIds = shows.Map(s => s.ItemId).ToList();
var showIds = existingShows var showIds = existingShows
@ -104,6 +113,7 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne
string address, string address,
string apiKey, string apiKey,
JellyfinLibrary library, JellyfinLibrary library,
string ffmpegPath,
string ffprobePath, string ffprobePath,
List<JellyfinPathReplacement> pathReplacements, List<JellyfinPathReplacement> pathReplacements,
List<JellyfinItemEtag> existingShows, List<JellyfinItemEtag> existingShows,
@ -167,6 +177,7 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne
address, address,
apiKey, apiKey,
library, library,
ffmpegPath,
ffprobePath, ffprobePath,
pathReplacements, pathReplacements,
incoming, incoming,
@ -196,6 +207,7 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne
string address, string address,
string apiKey, string apiKey,
JellyfinLibrary library, JellyfinLibrary library,
string ffmpegPath,
string ffprobePath, string ffprobePath,
List<JellyfinPathReplacement> pathReplacements, List<JellyfinPathReplacement> pathReplacements,
JellyfinShow show, JellyfinShow show,
@ -286,6 +298,7 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne
show.ShowMetadata.Head().Title, show.ShowMetadata.Head().Title,
incoming.SeasonMetadata.Head().Title, incoming.SeasonMetadata.Head().Title,
library, library,
ffmpegPath,
ffprobePath, ffprobePath,
pathReplacements, pathReplacements,
incoming, incoming,
@ -319,6 +332,7 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne
string showName, string showName,
string seasonName, string seasonName,
JellyfinLibrary library, JellyfinLibrary library,
string ffmpegPath,
string ffprobePath, string ffprobePath,
List<JellyfinPathReplacement> pathReplacements, List<JellyfinPathReplacement> pathReplacements,
JellyfinSeason season, JellyfinSeason season,
@ -408,7 +422,7 @@ public class JellyfinTelevisionLibraryScanner : IJellyfinTelevisionLibraryScanne
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath); _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath);
Either<BaseError, bool> refreshResult = Either<BaseError, bool> refreshResult =
await _localStatisticsProvider.RefreshStatistics(ffprobePath, incomingEpisode, localPath); await _localStatisticsProvider.RefreshStatistics(ffmpegPath, ffprobePath, incomingEpisode, localPath);
refreshResult.Match( refreshResult.Match(
_ => { }, _ => { },

3
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -79,6 +79,7 @@ public abstract class LocalFolderScanner
protected async Task<Either<BaseError, MediaItemScanResult<T>>> UpdateStatistics<T>( protected async Task<Either<BaseError, MediaItemScanResult<T>>> UpdateStatistics<T>(
MediaItemScanResult<T> mediaItem, MediaItemScanResult<T> mediaItem,
string ffmpegPath,
string ffprobePath) string ffprobePath)
where T : MediaItem where T : MediaItem
{ {
@ -92,7 +93,7 @@ public abstract class LocalFolderScanner
{ {
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", path); _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", path);
Either<BaseError, bool> refreshResult = Either<BaseError, bool> refreshResult =
await _localStatisticsProvider.RefreshStatistics(ffprobePath, mediaItem.Item); await _localStatisticsProvider.RefreshStatistics(ffmpegPath, ffprobePath, mediaItem.Item);
refreshResult.Match( refreshResult.Match(
result => result =>
{ {

86
ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs

@ -1,6 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using Bugsnag; using Bugsnag;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Extensions;
@ -30,12 +31,12 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
_logger = logger; _logger = logger;
} }
public async Task<Either<BaseError, bool>> RefreshStatistics(string ffprobePath, MediaItem mediaItem) public async Task<Either<BaseError, bool>> RefreshStatistics(string ffmpegPath, string ffprobePath, MediaItem mediaItem)
{ {
try try
{ {
string filePath = mediaItem.GetHeadVersion().MediaFiles.Head().Path; string filePath = mediaItem.GetHeadVersion().MediaFiles.Head().Path;
return await RefreshStatistics(ffprobePath, mediaItem, filePath); return await RefreshStatistics(ffmpegPath, ffprobePath, mediaItem, filePath);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -46,6 +47,7 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
} }
public async Task<Either<BaseError, bool>> RefreshStatistics( public async Task<Either<BaseError, bool>> RefreshStatistics(
string ffmpegPath,
string ffprobePath, string ffprobePath,
MediaItem mediaItem, MediaItem mediaItem,
string mediaItemPath) string mediaItemPath)
@ -57,6 +59,11 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
async ffprobe => async ffprobe =>
{ {
MediaVersion version = ProjectToMediaVersion(mediaItemPath, ffprobe); MediaVersion version = ProjectToMediaVersion(mediaItemPath, ffprobe);
if (version.Duration.TotalSeconds < 1)
{
await AnalyzeDuration(ffmpegPath, mediaItemPath, version);
}
bool result = await ApplyVersionUpdate(mediaItem, version, mediaItemPath); bool result = await ApplyVersionUpdate(mediaItem, version, mediaItemPath);
return Right<BaseError, bool>(result); return Right<BaseError, bool>(result);
}, },
@ -185,6 +192,67 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
}); });
} }
private async Task AnalyzeDuration(string ffmpegPath, string path, MediaVersion version)
{
try
{
_logger.LogInformation(
"Media item at {Path} is missing duration metadata and requires additional analysis",
path);
var startInfo = new ProcessStartInfo
{
FileName = ffmpegPath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(path);
startInfo.ArgumentList.Add("-f");
startInfo.ArgumentList.Add("null");
startInfo.ArgumentList.Add("-");
var probe = new Process
{
StartInfo = startInfo
};
probe.Start();
string output = await probe.StandardError.ReadToEndAsync();
await probe.WaitForExitAsync();
if (probe.ExitCode == 0)
{
const string PATTERN = @"time=([^ ]+)";
IEnumerable<string> reversed = output.Split("\n").Reverse();
foreach (string line in reversed)
{
Match match = Regex.Match(line, PATTERN);
if (match.Success)
{
string time = match.Groups[1].Value;
var duration = TimeSpan.Parse(time, NumberFormatInfo.InvariantInfo);
_logger.LogInformation("Analyzed duration is {Duration}", duration);
version.Duration = duration;
return;
}
}
}
else
{
_logger.LogError("Duration analysis failed for media item at {Path}", path);
}
}
catch (Exception ex)
{
_client.Notify(ex);
_logger.LogError("Duration analysis failed for media item at {Path}", path);
}
}
internal MediaVersion ProjectToMediaVersion(string path, FFprobe probeOutput) => internal MediaVersion ProjectToMediaVersion(string path, FFprobe probeOutput) =>
Optional(probeOutput) Optional(probeOutput)
.Filter(json => json?.format != null && json.streams != null) .Filter(json => json?.format != null && json.streams != null)
@ -210,13 +278,13 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
var seconds = TimeSpan.FromSeconds(duration); var seconds = TimeSpan.FromSeconds(duration);
version.Duration = seconds; version.Duration = seconds;
} }
else // else
{ // {
_logger.LogWarning( // _logger.LogWarning(
"Media item at {Path} has a missing or invalid duration {Duration} and will cause scheduling issues", // "Media item at {Path} has a missing or invalid duration {Duration} and will cause scheduling issues",
path, // path,
json.format.duration); // json.format.duration);
} // }
foreach (FFprobeStream audioStream in json.streams.Filter(s => s.codec_type == "audio")) foreach (FFprobeStream audioStream in json.streams.Filter(s => s.codec_type == "audio"))
{ {

3
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -64,6 +64,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
public async Task<Either<BaseError, Unit>> ScanFolder( public async Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffmpegPath,
string ffprobePath, string ffprobePath,
decimal progressMin, decimal progressMin,
decimal progressMax) decimal progressMax)
@ -131,7 +132,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
// TODO: figure out how to rebuild playlists // TODO: figure out how to rebuild playlists
Either<BaseError, MediaItemScanResult<Movie>> maybeMovie = await _movieRepository Either<BaseError, MediaItemScanResult<Movie>> maybeMovie = await _movieRepository
.GetOrAdd(libraryPath, file) .GetOrAdd(libraryPath, file)
.BindT(movie => UpdateStatistics(movie, ffprobePath)) .BindT(movie => UpdateStatistics(movie, ffmpegPath, ffprobePath))
.BindT(UpdateMetadata) .BindT(UpdateMetadata)
.BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster)) .BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster))
.BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt)) .BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt))

5
ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs

@ -65,6 +65,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
public async Task<Either<BaseError, Unit>> ScanFolder( public async Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffmpegPath,
string ffprobePath, string ffprobePath,
decimal progressMin, decimal progressMin,
decimal progressMax) decimal progressMax)
@ -95,6 +96,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
{ {
await ScanMusicVideos( await ScanMusicVideos(
libraryPath, libraryPath,
ffmpegPath,
ffprobePath, ffprobePath,
result.Item, result.Item,
artistFolder); artistFolder);
@ -233,6 +235,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
private async Task ScanMusicVideos( private async Task ScanMusicVideos(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffmpegPath,
string ffprobePath, string ffprobePath,
Artist artist, Artist artist,
string artistFolder) string artistFolder)
@ -272,7 +275,7 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
// TODO: figure out how to rebuild playouts // TODO: figure out how to rebuild playouts
Either<BaseError, MediaItemScanResult<MusicVideo>> maybeMusicVideo = await _musicVideoRepository Either<BaseError, MediaItemScanResult<MusicVideo>> maybeMusicVideo = await _musicVideoRepository
.GetOrAdd(artist, libraryPath, file) .GetOrAdd(artist, libraryPath, file)
.BindT(musicVideo => UpdateStatistics(musicVideo, ffprobePath)) .BindT(musicVideo => UpdateStatistics(musicVideo, ffmpegPath, ffprobePath))
.BindT(UpdateMetadata) .BindT(UpdateMetadata)
.BindT(UpdateThumbnail) .BindT(UpdateThumbnail)
.BindT(FlagNormal); .BindT(FlagNormal);

3
ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs

@ -62,6 +62,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
public async Task<Either<BaseError, Unit>> ScanFolder( public async Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffmpegPath,
string ffprobePath, string ffprobePath,
decimal progressMin, decimal progressMin,
decimal progressMax) decimal progressMax)
@ -126,7 +127,7 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
{ {
Either<BaseError, MediaItemScanResult<OtherVideo>> maybeVideo = await _otherVideoRepository Either<BaseError, MediaItemScanResult<OtherVideo>> maybeVideo = await _otherVideoRepository
.GetOrAdd(libraryPath, file) .GetOrAdd(libraryPath, file)
.BindT(video => UpdateStatistics(video, ffprobePath)) .BindT(video => UpdateStatistics(video, ffmpegPath, ffprobePath))
.BindT(UpdateMetadata) .BindT(UpdateMetadata)
.BindT(FlagNormal); .BindT(FlagNormal);

2
ErsatzTV.Core/Metadata/SongFolderScanner.cs

@ -128,7 +128,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
{ {
Either<BaseError, MediaItemScanResult<Song>> maybeSong = await _songRepository Either<BaseError, MediaItemScanResult<Song>> maybeSong = await _songRepository
.GetOrAdd(libraryPath, file) .GetOrAdd(libraryPath, file)
.BindT(video => UpdateStatistics(video, ffprobePath)) .BindT(video => UpdateStatistics(video, ffmpegPath, ffprobePath))
.BindT(video => UpdateMetadata(video, ffprobePath)) .BindT(video => UpdateMetadata(video, ffprobePath))
.BindT(video => UpdateThumbnail(video, ffmpegPath)) .BindT(video => UpdateThumbnail(video, ffmpegPath))
.BindT(FlagNormal); .BindT(FlagNormal);

8
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -64,6 +64,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
public async Task<Either<BaseError, Unit>> ScanFolder( public async Task<Either<BaseError, Unit>> ScanFolder(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffmpegPath,
string ffprobePath, string ffprobePath,
decimal progressMin, decimal progressMin,
decimal progressMax) decimal progressMax)
@ -93,6 +94,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
{ {
await ScanSeasons( await ScanSeasons(
libraryPath, libraryPath,
ffmpegPath,
ffprobePath, ffprobePath,
result.Item, result.Item,
showFolder); showFolder);
@ -154,6 +156,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
private async Task<Unit> ScanSeasons( private async Task<Unit> ScanSeasons(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffmpegPath,
string ffprobePath, string ffprobePath,
Show show, Show show,
string showFolder) string showFolder)
@ -184,7 +187,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
await maybeSeason.Match( await maybeSeason.Match(
async season => async season =>
{ {
await ScanEpisodes(libraryPath, ffprobePath, season, seasonFolder); await ScanEpisodes(libraryPath, ffmpegPath, ffprobePath, season, seasonFolder);
await _libraryRepository.SetEtag(libraryPath, knownFolder, seasonFolder, etag); await _libraryRepository.SetEtag(libraryPath, knownFolder, seasonFolder, etag);
season.Show = show; season.Show = show;
@ -206,6 +209,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
private async Task<Unit> ScanEpisodes( private async Task<Unit> ScanEpisodes(
LibraryPath libraryPath, LibraryPath libraryPath,
string ffmpegPath,
string ffprobePath, string ffprobePath,
Season season, Season season,
string seasonPath) string seasonPath)
@ -225,7 +229,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
Either<BaseError, Episode> maybeEpisode = await _televisionRepository Either<BaseError, Episode> maybeEpisode = await _televisionRepository
.GetOrAddEpisode(season, libraryPath, file) .GetOrAddEpisode(season, libraryPath, file)
.BindT( .BindT(
episode => UpdateStatistics(new MediaItemScanResult<Episode>(episode), ffprobePath) episode => UpdateStatistics(new MediaItemScanResult<Episode>(episode), ffmpegPath, ffprobePath)
.MapT(_ => episode)) .MapT(_ => episode))
.BindT(UpdateMetadata) .BindT(UpdateMetadata)
.BindT(UpdateThumbnail) .BindT(UpdateThumbnail)

7
ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs

@ -54,6 +54,7 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
PlexLibrary library, PlexLibrary library,
string ffmpegPath,
string ffprobePath) string ffprobePath)
{ {
List<PlexPathReplacement> pathReplacements = await _mediaSourceRepository List<PlexPathReplacement> pathReplacements = await _mediaSourceRepository
@ -93,7 +94,8 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan
// TODO: figure out how to rebuild playlists // TODO: figure out how to rebuild playlists
Either<BaseError, MediaItemScanResult<PlexMovie>> maybeMovie = await _movieRepository Either<BaseError, MediaItemScanResult<PlexMovie>> maybeMovie = await _movieRepository
.GetOrAdd(library, incoming) .GetOrAdd(library, incoming)
.BindT(existing => UpdateStatistics(pathReplacements, existing, incoming, ffprobePath)) .BindT(
existing => UpdateStatistics(pathReplacements, existing, incoming, ffmpegPath, ffprobePath))
.BindT(existing => UpdateMetadata(existing, incoming, library, connection, token)) .BindT(existing => UpdateMetadata(existing, incoming, library, connection, token))
.BindT(existing => UpdateArtwork(existing, incoming)); .BindT(existing => UpdateArtwork(existing, incoming));
@ -145,6 +147,7 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan
List<PlexPathReplacement> pathReplacements, List<PlexPathReplacement> pathReplacements,
MediaItemScanResult<PlexMovie> result, MediaItemScanResult<PlexMovie> result,
PlexMovie incoming, PlexMovie incoming,
string ffmpegPath,
string ffprobePath) string ffprobePath)
{ {
PlexMovie existing = result.Item; PlexMovie existing = result.Item;
@ -178,7 +181,7 @@ public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScan
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath); _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath);
Either<BaseError, bool> refreshResult = Either<BaseError, bool> refreshResult =
await _localStatisticsProvider.RefreshStatistics(ffprobePath, existing, localPath); await _localStatisticsProvider.RefreshStatistics(ffmpegPath, ffprobePath, existing, localPath);
await refreshResult.Match( await refreshResult.Match(
async _ => async _ =>

17
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

@ -55,6 +55,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
PlexLibrary library, PlexLibrary library,
string ffmpegPath,
string ffprobePath) string ffprobePath)
{ {
List<PlexPathReplacement> pathReplacements = await _mediaSourceRepository List<PlexPathReplacement> pathReplacements = await _mediaSourceRepository
@ -82,7 +83,14 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
await maybeShow.Match( await maybeShow.Match(
async result => async result =>
{ {
await ScanSeasons(library, pathReplacements, result.Item, connection, token, ffprobePath); await ScanSeasons(
library,
pathReplacements,
result.Item,
connection,
token,
ffmpegPath,
ffprobePath);
if (result.IsAdded) if (result.IsAdded)
{ {
@ -286,6 +294,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
PlexShow show, PlexShow show,
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
string ffmpegPath,
string ffprobePath) string ffprobePath)
{ {
Either<BaseError, List<PlexSeason>> entries = await _plexServerApiClient.GetShowSeasons( Either<BaseError, List<PlexSeason>> entries = await _plexServerApiClient.GetShowSeasons(
@ -315,6 +324,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
season, season,
connection, connection,
token, token,
ffmpegPath,
ffprobePath); ffprobePath);
season.Show = show; season.Show = show;
@ -384,6 +394,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
PlexSeason season, PlexSeason season,
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
string ffmpegPath,
string ffprobePath) string ffprobePath)
{ {
Either<BaseError, List<PlexEpisode>> entries = await _plexServerApiClient.GetSeasonEpisodes( Either<BaseError, List<PlexEpisode>> entries = await _plexServerApiClient.GetSeasonEpisodes(
@ -431,6 +442,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
library, library,
connection, connection,
token, token,
ffmpegPath,
ffprobePath)) ffprobePath))
.BindT(existing => UpdateArtwork(existing, incoming)); .BindT(existing => UpdateArtwork(existing, incoming));
@ -503,6 +515,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
PlexLibrary library, PlexLibrary library,
PlexConnection connection, PlexConnection connection,
PlexServerAuthToken token, PlexServerAuthToken token,
string ffmpegPath,
string ffprobePath) string ffprobePath)
{ {
MediaVersion existingVersion = existing.MediaVersions.Head(); MediaVersion existingVersion = existing.MediaVersions.Head();
@ -535,7 +548,7 @@ public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionL
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath); _logger.LogDebug("Refreshing {Attribute} for {Path}", "Statistics", localPath);
Either<BaseError, bool> refreshResult = Either<BaseError, bool> refreshResult =
await _localStatisticsProvider.RefreshStatistics(ffprobePath, existing, localPath); await _localStatisticsProvider.RefreshStatistics(ffmpegPath, ffprobePath, existing, localPath);
await refreshResult.Match( await refreshResult.Match(
async _ => async _ =>

3886
ErsatzTV.Infrastructure/Migrations/20220308033129_Analyze_ZeroDurationFiles.Designer.cs generated

File diff suppressed because it is too large Load Diff

50
ErsatzTV.Infrastructure/Migrations/20220308033129_Analyze_ZeroDurationFiles.cs

@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Analyze_ZeroDurationFiles : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"UPDATE LibraryFolder SET Etag = NULL WHERE Id IN
(
SELECT LF.Id FROM LibraryFolder LF
INNER JOIN LibraryPath LP on LF.LibraryPathId = LP.Id
INNER JOIN Library L on LP.LibraryId = L.Id
INNER JOIN MediaItem MI on LP.Id = MI.LibraryPathId
INNER JOIN MediaVersion MV on MI.Id = COALESCE(MovieId, MusicVideoId, OtherVideoId, SongId, EpisodeId)
WHERE MV.Duration = '00:00:00.0000000'
)");
migrationBuilder.Sql(
@"UPDATE LibraryPath SET LastScan = '0001-01-01 00:00:00' WHERE Id IN
(
SELECT LP.Id FROM LibraryPath LP
INNER JOIN Library L on LP.LibraryId = L.Id
INNER JOIN MediaItem MI on LP.Id = MI.LibraryPathId
INNER JOIN MediaVersion MV on MI.Id = COALESCE(MovieId, MusicVideoId, OtherVideoId, SongId, EpisodeId)
WHERE MV.Duration = '00:00:00.0000000'
)");
migrationBuilder.Sql(
@"UPDATE Library SET LastScan = '0001-01-01 00:00:00' WHERE Id IN
(
SELECT L.Id FROM Library L
INNER JOIN LibraryPath LP on L.Id = LP.LibraryId
INNER JOIN MediaItem MI on LP.Id = MI.LibraryPathId
INNER JOIN MediaVersion MV on MI.Id = COALESCE(MovieId, MusicVideoId, OtherVideoId, SongId, EpisodeId)
WHERE MV.Duration = '00:00:00.0000000'
)");
migrationBuilder.Sql(
@"UPDATE MediaVersion SET DateUpdated = '0001-01-01 00:00:00' WHERE Duration = '00:00:00.0000000'");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
Loading…
Cancel
Save