Browse Source

add duplicate file logging (#1223)

pull/1224/head
Jason Dove 3 years ago committed by GitHub
parent
commit
307940d732
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 4
      ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs
  3. 8
      ErsatzTV.Core/Errors/MediaFileAlreadyExists.cs
  4. 14
      ErsatzTV.Infrastructure/Data/Repositories/EmbyMovieRepository.cs
  5. 15
      ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs
  6. 15
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinMovieRepository.cs
  7. 15
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs
  8. 51
      ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs
  9. 17
      ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs
  10. 14
      ErsatzTV.Infrastructure/Data/Repositories/PlexMovieRepository.cs
  11. 7
      ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs
  12. 16
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

2
CHANGELOG.md

@ -6,10 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

4
ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs

@ -115,7 +115,7 @@ public class ScheduleIntegrationTests @@ -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 @@ -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,

8
ErsatzTV.Core/Errors/MediaFileAlreadyExists.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.Core.Errors;
public class MediaFileAlreadyExists : BaseError
{
public MediaFileAlreadyExists() : base("Media file already exists")
{
}
}

14
ErsatzTV.Infrastructure/Data/Repositories/EmbyMovieRepository.cs

@ -2,18 +2,25 @@ @@ -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 @@ -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;

15
ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs

@ -2,19 +2,27 @@ @@ -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 @@ -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;

15
ErsatzTV.Infrastructure/Data/Repositories/JellyfinMovieRepository.cs

@ -1,20 +1,28 @@ @@ -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 @@ -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;

15
ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs

@ -1,20 +1,28 @@ @@ -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 @@ -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;

51
ErsatzTV.Infrastructure/Data/Repositories/MediaItemRepository.cs

@ -3,9 +3,11 @@ using System.Globalization; @@ -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 @@ -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 @@ -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();

17
ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs

@ -1,17 +1,24 @@ @@ -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 @@ -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 @@ -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,

14
ErsatzTV.Infrastructure/Data/Repositories/PlexMovieRepository.cs

@ -1,19 +1,26 @@ @@ -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 @@ -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;

7
ErsatzTV.Infrastructure/Data/Repositories/PlexTelevisionRepository.cs

@ -1,6 +1,7 @@ @@ -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 @@ -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

16
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -1,17 +1,24 @@ @@ -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 @@ -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 @@ -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 @@ -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

Loading…
Cancel
Save