add duplicate file logging (#1223)

This commit is contained in:
Jason Dove
2023-04-01 09:54:19 -05:00
committed by GitHub
parent 721f0df82a
commit 307940d732
12 changed files with 162 additions and 16 deletions
+2
View File
@@ -6,10 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Use `plot` field from Other Video NFO metadata as XMLTV description
- Add detailed warning log when a file is added to ErsatzTV more than once
### Fixed
- Fix updating (re-adding) Trakt lists to properly use new metadata ids that were not present when originally added
- Fix local show library scanning with non-english season folder names, e.g. `Staffel 02`
- Fix bug where local libraries would merge with media server libraries when the same file was added to both libraries
### Changed
- Use Poster artwork for XMLTV if available
@@ -115,7 +115,7 @@ public class ScheduleIntegrationTests
var builder = new PlayoutBuilder(
new ConfigElementRepository(factory),
new MediaCollectionRepository(new Mock<IClient>().Object, searchIndex, factory),
new TelevisionRepository(factory),
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),
new ArtistRepository(factory),
new Mock<IMultiEpisodeShuffleCollectionEnumeratorFactory>().Object,
new Mock<ILocalFileSystem>().Object,
@@ -275,7 +275,7 @@ public class ScheduleIntegrationTests
var builder = new PlayoutBuilder(
new ConfigElementRepository(factory),
new MediaCollectionRepository(new Mock<IClient>().Object, new Mock<ISearchIndex>().Object, factory),
new TelevisionRepository(factory),
new TelevisionRepository(factory, provider.GetRequiredService<ILogger<TelevisionRepository>>()),
new ArtistRepository(factory),
new Mock<IMultiEpisodeShuffleCollectionEnumeratorFactory>().Object,
new Mock<ILocalFileSystem>().Object,
@@ -0,0 +1,8 @@
namespace ErsatzTV.Core.Errors;
public class MediaFileAlreadyExists : BaseError
{
public MediaFileAlreadyExists() : base("Media file already exists")
{
}
}
@@ -2,18 +2,25 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class EmbyMovieRepository : IEmbyMovieRepository
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<EmbyMovieRepository> _logger;
public EmbyMovieRepository(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public EmbyMovieRepository(IDbContextFactory<TvContext> dbContextFactory, ILogger<EmbyMovieRepository> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<List<EmbyItemEtag>> GetExistingMovies(EmbyLibrary library)
{
@@ -178,6 +185,11 @@ public class EmbyMovieRepository : IEmbyMovieRepository
{
try
{
if (await MediaItemRepository.MediaFileAlreadyExists(movie, dbContext, _logger))
{
return new MediaFileAlreadyExists();
}
// blank out etag for initial save in case other updates fail
string etag = movie.Etag;
movie.Etag = string.Empty;
@@ -2,19 +2,27 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class EmbyTelevisionRepository : IEmbyTelevisionRepository
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<EmbyTelevisionRepository> _logger;
public EmbyTelevisionRepository(IDbContextFactory<TvContext> dbContextFactory) =>
public EmbyTelevisionRepository(
IDbContextFactory<TvContext> dbContextFactory,
ILogger<EmbyTelevisionRepository> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<List<EmbyItemEtag>> GetExistingShows(EmbyLibrary library)
{
@@ -801,6 +809,11 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
{
try
{
if (await MediaItemRepository.MediaFileAlreadyExists(episode, dbContext, _logger))
{
return new MediaFileAlreadyExists();
}
// blank out etag for initial save in case other updates fail
string etag = episode.Etag;
episode.Etag = string.Empty;
@@ -1,20 +1,28 @@
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class JellyfinMovieRepository : IJellyfinMovieRepository
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<JellyfinMovieRepository> _logger;
public JellyfinMovieRepository(IDbContextFactory<TvContext> dbContextFactory) =>
public JellyfinMovieRepository(
IDbContextFactory<TvContext> dbContextFactory,
ILogger<JellyfinMovieRepository> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<List<JellyfinItemEtag>> GetExistingMovies(JellyfinLibrary library)
{
@@ -359,6 +367,11 @@ public class JellyfinMovieRepository : IJellyfinMovieRepository
{
try
{
if (await MediaItemRepository.MediaFileAlreadyExists(movie, dbContext, _logger))
{
return new MediaFileAlreadyExists();
}
// blank out etag for initial save in case other updates fail
string etag = movie.Etag;
movie.Etag = string.Empty;
@@ -1,20 +1,28 @@
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<JellyfinTelevisionRepository> _logger;
public JellyfinTelevisionRepository(IDbContextFactory<TvContext> dbContextFactory) =>
public JellyfinTelevisionRepository(
IDbContextFactory<TvContext> dbContextFactory,
ILogger<JellyfinTelevisionRepository> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<List<JellyfinItemEtag>> GetExistingShows(JellyfinLibrary library)
{
@@ -804,6 +812,11 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
{
try
{
if (await MediaItemRepository.MediaFileAlreadyExists(episode, dbContext, _logger))
{
return new MediaFileAlreadyExists();
}
// blank out etag for initial save in case other updates fail
string etag = episode.Etag;
episode.Etag = string.Empty;
@@ -3,9 +3,11 @@ using System.Globalization;
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Infrastructure.Data.Repositories;
@@ -103,6 +105,7 @@ public class MediaItemRepository : IMediaItemRepository
mediaItem.State = MediaItemState.Normal;
return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id",
new { mediaItem.Id }).ToUnit();
}
@@ -121,6 +124,54 @@ public class MediaItemRepository : IMediaItemRepository
return Unit.Default;
}
public static async Task<bool> MediaFileAlreadyExists(MediaItem incoming, TvContext dbContext, ILogger logger)
{
string path = incoming.GetHeadVersion().MediaFiles.Head().Path;
return await MediaFileAlreadyExists(path, dbContext, logger);
}
public static async Task<bool> MediaFileAlreadyExists(string path, TvContext dbContext, ILogger logger)
{
Option<int> maybeMediaItemId = await dbContext.Connection
.QuerySingleOrDefaultAsync<int?>(
@"select coalesce(EpisodeId, MovieId, MusicVideoId, OtherVideoId, SongId) as MediaItemId
from MediaVersion MV
inner join MediaFile MF on MV.Id = MF.MediaVersionId
where MF.Path = @Path",
new { Path = path })
.Map(Optional);
foreach (int mediaItemId in maybeMediaItemId)
{
Option<MediaItem> maybeMediaItem = await dbContext.MediaItems
.AsNoTracking()
.Include(mi => mi.LibraryPath)
.ThenInclude(lp => lp.Library)
.SelectOneAsync(mi => mi.Id, mi => mi.Id == mediaItemId);
foreach (MediaItem mediaItem in maybeMediaItem)
{
string libraryType = mediaItem.LibraryPath.Library switch
{
PlexLibrary => "Plex Library",
EmbyLibrary => "Emby Library",
JellyfinLibrary => "Jellyfin Library",
_ => "Local Library"
};
string libraryName = mediaItem.LibraryPath.Library.Name;
logger.LogWarning(
"Unable to add media item; {LibraryType} '{LibraryName}' already contains path {Path}",
libraryType,
libraryName,
path);
return true;
}
}
return false;
}
private async Task<List<string>> GetAllLanguageCodes()
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@@ -1,17 +1,24 @@
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class MovieRepository : IMovieRepository
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<MovieRepository> _logger;
public MovieRepository(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public MovieRepository(IDbContextFactory<TvContext> dbContextFactory, ILogger<MovieRepository> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<bool> AllMoviesExist(List<int> movieIds)
{
@@ -54,6 +61,7 @@ public class MovieRepository : IMovieRepository
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<Movie> maybeExisting = await dbContext.Movies
.Filter(m => !(m is PlexMovie) && !(m is JellyfinMovie) && !(m is EmbyMovie))
.Include(i => i.MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(i => i.MovieMetadata)
@@ -219,13 +227,18 @@ public class MovieRepository : IMovieRepository
new { writer.Name, MetadataId = metadata.Id }).Map(result => result > 0);
}
private static async Task<Either<BaseError, MediaItemScanResult<Movie>>> AddMovie(
private async Task<Either<BaseError, MediaItemScanResult<Movie>>> AddMovie(
TvContext dbContext,
int libraryPathId,
string path)
{
try
{
if (await MediaItemRepository.MediaFileAlreadyExists(path, dbContext, _logger))
{
return new MediaFileAlreadyExists();
}
var movie = new Movie
{
LibraryPathId = libraryPathId,
@@ -1,19 +1,26 @@
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Plex;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class PlexMovieRepository : IPlexMovieRepository
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<PlexMovieRepository> _logger;
public PlexMovieRepository(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public PlexMovieRepository(IDbContextFactory<TvContext> dbContextFactory, ILogger<PlexMovieRepository> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<List<PlexItemEtag>> GetExistingMovies(PlexLibrary library)
{
@@ -178,6 +185,11 @@ public class PlexMovieRepository : IPlexMovieRepository
{
try
{
if (await MediaItemRepository.MediaFileAlreadyExists(item, dbContext, _logger))
{
return new MediaFileAlreadyExists();
}
// blank out etag for initial save in case stats/metadata/etc updates fail
string etag = item.Etag;
item.Etag = string.Empty;
@@ -1,6 +1,7 @@
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Plex;
@@ -425,16 +426,16 @@ public class PlexTelevisionRepository : IPlexTelevisionRepository
}
}
private static async Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>> AddEpisode(
private async Task<Either<BaseError, MediaItemScanResult<PlexEpisode>>> AddEpisode(
TvContext dbContext,
PlexLibrary library,
PlexEpisode item)
{
try
{
if (dbContext.MediaFiles.Any(mf => mf.Path == item.MediaVersions.Head().MediaFiles.Head().Path))
if (await MediaItemRepository.MediaFileAlreadyExists(item, dbContext, _logger))
{
return BaseError.New("Multi-episode files are not yet supported");
return new MediaFileAlreadyExists();
}
// blank out etag for initial save in case stats/metadata/etc updates fail
@@ -1,17 +1,24 @@
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class TelevisionRepository : ITelevisionRepository
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<TelevisionRepository> _logger;
public TelevisionRepository(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public TelevisionRepository(IDbContextFactory<TvContext> dbContextFactory, ILogger<TelevisionRepository> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<bool> AllShowsExist(List<int> showIds)
{
@@ -325,6 +332,7 @@ public class TelevisionRepository : ITelevisionRepository
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<Episode> maybeExisting = await dbContext.Episodes
.Filter(e => !(e is PlexEpisode) && !(e is JellyfinEpisode) && !(e is EmbyEpisode))
.Include(i => i.EpisodeMetadata)
.ThenInclude(em => em.Artwork)
.Include(i => i.EpisodeMetadata)
@@ -670,7 +678,7 @@ public class TelevisionRepository : ITelevisionRepository
}
}
private static async Task<Either<BaseError, Episode>> AddEpisode(
private async Task<Either<BaseError, Episode>> AddEpisode(
TvContext dbContext,
Season season,
int libraryPathId,
@@ -678,9 +686,9 @@ public class TelevisionRepository : ITelevisionRepository
{
try
{
if (dbContext.MediaFiles.Any(mf => mf.Path == path))
if (await MediaItemRepository.MediaFileAlreadyExists(path, dbContext, _logger))
{
return BaseError.New("Multi-episode files are not yet supported");
return new MediaFileAlreadyExists();
}
var episode = new Episode