add duplicate file logging (#1223)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user