Browse Source

store local library folder hierarchy in db (#1616)

pull/1617/head
Jason Dove 1 year ago committed by GitHub
parent
commit
71e9ea867a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 6
      ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs
  3. 4
      ErsatzTV.Core/Domain/Library/LibraryFolder.cs
  4. 3
      ErsatzTV.Core/Domain/MediaItem/MediaFile.cs
  5. 5
      ErsatzTV.Core/Interfaces/Repositories/IImageRepository.cs
  6. 8
      ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs
  7. 7
      ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs
  8. 1
      ErsatzTV.Core/Interfaces/Repositories/IMusicVideoRepository.cs
  9. 5
      ErsatzTV.Core/Interfaces/Repositories/IOtherVideoRepository.cs
  10. 5
      ErsatzTV.Core/Interfaces/Repositories/ISongRepository.cs
  11. 8
      ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs
  12. 5136
      ErsatzTV.Infrastructure.MySql/Migrations/20240216120724_Rework_LibraryFolder.Designer.cs
  13. 80
      ErsatzTV.Infrastructure.MySql/Migrations/20240216120724_Rework_LibraryFolder.cs
  14. 5136
      ErsatzTV.Infrastructure.MySql/Migrations/20240216120800_Reset_LocalLibraryFolders.Designer.cs
  15. 31
      ErsatzTV.Infrastructure.MySql/Migrations/20240216120800_Reset_LocalLibraryFolders.cs
  16. 31
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  17. 5134
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240216022703_Rework_LibraryFolder.Designer.cs
  18. 80
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240216022703_Rework_LibraryFolder.cs
  19. 5134
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240216022831_Reset_LocalLibraryFolders.Designer.cs
  20. 31
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240216022831_Reset_LocalLibraryFolders.cs
  21. 31
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  22. 11
      ErsatzTV.Infrastructure/Data/Configurations/Library/LibraryFolderConfiguration.cs
  23. 6
      ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MediaFileConfiguration.cs
  24. 16
      ErsatzTV.Infrastructure/Data/Repositories/ImageRepository.cs
  25. 152
      ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs
  26. 25
      ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs
  27. 24
      ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs
  28. 21
      ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs
  29. 21
      ErsatzTV.Infrastructure/Data/Repositories/SongRepository.cs
  30. 43
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  31. 55
      ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs
  32. 42
      ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs
  33. 43
      ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs
  34. 40
      ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs
  35. 39
      ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs
  36. 43
      ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs
  37. 44
      ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs

1
CHANGELOG.md

@ -36,6 +36,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -36,6 +36,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix playback of media items with no audio streams
- Fix timestamp continuity in `HLS Segmenter` sessions
- This should make *some* clients happier
- Fix `Other Video`, `Song` and `Image` fallback metadata tags to always include parent folder (folder added to library)
### Changed
- Log search index updates under scanner category at debug level, to indicate a potential cause for the UI being out of date

6
ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs

@ -47,7 +47,11 @@ public class FakeTelevisionRepository : ITelevisionRepository @@ -47,7 +47,11 @@ public class FakeTelevisionRepository : ITelevisionRepository
public Task<Either<BaseError, Season>> GetOrAddSeason(Show show, int libraryPathId, int seasonNumber) =>
throw new NotSupportedException();
public Task<Either<BaseError, Episode>> GetOrAddEpisode(Season season, LibraryPath libraryPath, string path) =>
public Task<Either<BaseError, Episode>> GetOrAddEpisode(
Season season,
LibraryPath libraryPath,
LibraryFolder libraryFolder,
string path) =>
throw new NotSupportedException();
public Task<IEnumerable<string>> FindEpisodePaths(LibraryPath libraryPath) => throw new NotSupportedException();

4
ErsatzTV.Core/Domain/Library/LibraryFolder.cs

@ -6,5 +6,9 @@ public class LibraryFolder @@ -6,5 +6,9 @@ public class LibraryFolder
public string Path { get; set; }
public int LibraryPathId { get; set; }
public LibraryPath LibraryPath { get; set; }
public int? ParentId { get; set; }
public LibraryFolder Parent { get; set; }
public ICollection<LibraryFolder> Children { get; set; }
public ICollection<MediaFile> MediaFiles { get; set; }
public string Etag { get; set; }
}

3
ErsatzTV.Core/Domain/MediaItem/MediaFile.cs

@ -7,4 +7,7 @@ public class MediaFile @@ -7,4 +7,7 @@ public class MediaFile
public int MediaVersionId { get; set; }
public MediaVersion MediaVersion { get; set; }
public int? LibraryFolderId { get; set; }
public LibraryFolder LibraryFolder { get; set; }
}

5
ErsatzTV.Core/Interfaces/Repositories/IImageRepository.cs

@ -5,7 +5,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories; @@ -5,7 +5,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories;
public interface IImageRepository
{
Task<Either<BaseError, MediaItemScanResult<Image>>> GetOrAdd(LibraryPath libraryPath, string path);
Task<Either<BaseError, MediaItemScanResult<Image>>> GetOrAdd(
LibraryPath libraryPath,
LibraryFolder libraryFolder,
string path);
Task<IEnumerable<string>> FindImagePaths(LibraryPath libraryPath);
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);
Task<bool> AddTag(ImageMetadata metadata, Tag tag);

8
ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs

@ -12,6 +12,10 @@ public interface ILibraryRepository @@ -12,6 +12,10 @@ public interface ILibraryRepository
Task<Unit> UpdateLastScan(LibraryPath libraryPath);
Task<List<LibraryPath>> GetLocalPaths(int libraryId);
Task<int> CountMediaItemsByPath(int libraryPathId);
Task<Unit> SetEtag(LibraryPath libraryPath, Option<LibraryFolder> knownFolder, string path, string etag);
Task<Unit> CleanEtagsForLibraryPath(LibraryPath libraryPath);
Task SetEtag(LibraryPath libraryPath, Option<LibraryFolder> knownFolder, string path, string etag);
Task CleanEtagsForLibraryPath(LibraryPath libraryPath);
Task<Option<int>> GetParentFolderId(string folder);
Task<LibraryFolder> GetOrAddFolder(LibraryPath libraryPath, Option<int> maybeParentFolder, string folder);
Task UpdateLibraryFolderId(MediaFile mediaFile, int libraryFolderId);
Task UpdatePath(LibraryPath libraryPath, string normalizedLibraryPath);
}

7
ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs

@ -7,7 +7,12 @@ public interface IMovieRepository @@ -7,7 +7,12 @@ public interface IMovieRepository
{
Task<bool> AllMoviesExist(List<int> movieIds);
Task<Option<Movie>> GetMovie(int movieId);
Task<Either<BaseError, MediaItemScanResult<Movie>>> GetOrAdd(LibraryPath libraryPath, string path);
Task<Either<BaseError, MediaItemScanResult<Movie>>> GetOrAdd(
LibraryPath libraryPath,
LibraryFolder libraryFolder,
string path);
Task<List<MovieMetadata>> GetMoviesForCards(List<int> ids);
Task<IEnumerable<string>> FindMoviePaths(LibraryPath libraryPath);
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);

1
ErsatzTV.Core/Interfaces/Repositories/IMusicVideoRepository.cs

@ -8,6 +8,7 @@ public interface IMusicVideoRepository @@ -8,6 +8,7 @@ public interface IMusicVideoRepository
Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> GetOrAdd(
Artist artist,
LibraryPath libraryPath,
LibraryFolder libraryFolder,
string path);
Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath);

5
ErsatzTV.Core/Interfaces/Repositories/IOtherVideoRepository.cs

@ -5,7 +5,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories; @@ -5,7 +5,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories;
public interface IOtherVideoRepository
{
Task<Either<BaseError, MediaItemScanResult<OtherVideo>>> GetOrAdd(LibraryPath libraryPath, string path);
Task<Either<BaseError, MediaItemScanResult<OtherVideo>>> GetOrAdd(
LibraryPath libraryPath,
LibraryFolder libraryFolder,
string path);
Task<IEnumerable<string>> FindOtherVideoPaths(LibraryPath libraryPath);
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);
Task<bool> AddGenre(OtherVideoMetadata metadata, Genre genre);

5
ErsatzTV.Core/Interfaces/Repositories/ISongRepository.cs

@ -5,7 +5,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories; @@ -5,7 +5,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories;
public interface ISongRepository
{
Task<Either<BaseError, MediaItemScanResult<Song>>> GetOrAdd(LibraryPath libraryPath, string path);
Task<Either<BaseError, MediaItemScanResult<Song>>> GetOrAdd(
LibraryPath libraryPath,
LibraryFolder libraryFolder,
string path);
Task<IEnumerable<string>> FindSongPaths(LibraryPath libraryPath);
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);
Task<bool> AddGenre(SongMetadata metadata, Genre genre);

8
ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs

@ -24,7 +24,13 @@ public interface ITelevisionRepository @@ -24,7 +24,13 @@ public interface ITelevisionRepository
Task<Option<Show>> GetShowByMetadata(int libraryPathId, ShowMetadata metadata, string showFolder);
Task<Either<BaseError, MediaItemScanResult<Show>>> AddShow(int libraryPathId, ShowMetadata metadata);
Task<Either<BaseError, Season>> GetOrAddSeason(Show show, int libraryPathId, int seasonNumber);
Task<Either<BaseError, Episode>> GetOrAddEpisode(Season season, LibraryPath libraryPath, string path);
Task<Either<BaseError, Episode>> GetOrAddEpisode(
Season season,
LibraryPath libraryPath,
LibraryFolder libraryFolder,
string path);
Task<IEnumerable<string>> FindEpisodePaths(LibraryPath libraryPath);
Task<Unit> DeleteByPath(LibraryPath libraryPath, string path);
Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath);

5136
ErsatzTV.Infrastructure.MySql/Migrations/20240216120724_Rework_LibraryFolder.Designer.cs generated

File diff suppressed because it is too large Load Diff

80
ErsatzTV.Infrastructure.MySql/Migrations/20240216120724_Rework_LibraryFolder.cs

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Rework_LibraryFolder : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "LibraryFolderId",
table: "MediaFile",
type: "int",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "ParentId",
table: "LibraryFolder",
type: "int",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_MediaFile_LibraryFolderId",
table: "MediaFile",
column: "LibraryFolderId");
migrationBuilder.CreateIndex(
name: "IX_LibraryFolder_ParentId",
table: "LibraryFolder",
column: "ParentId");
migrationBuilder.AddForeignKey(
name: "FK_LibraryFolder_LibraryFolder_ParentId",
table: "LibraryFolder",
column: "ParentId",
principalTable: "LibraryFolder",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_MediaFile_LibraryFolder_LibraryFolderId",
table: "MediaFile",
column: "LibraryFolderId",
principalTable: "LibraryFolder",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_LibraryFolder_LibraryFolder_ParentId",
table: "LibraryFolder");
migrationBuilder.DropForeignKey(
name: "FK_MediaFile_LibraryFolder_LibraryFolderId",
table: "MediaFile");
migrationBuilder.DropIndex(
name: "IX_MediaFile_LibraryFolderId",
table: "MediaFile");
migrationBuilder.DropIndex(
name: "IX_LibraryFolder_ParentId",
table: "LibraryFolder");
migrationBuilder.DropColumn(
name: "LibraryFolderId",
table: "MediaFile");
migrationBuilder.DropColumn(
name: "ParentId",
table: "LibraryFolder");
}
}
}

5136
ErsatzTV.Infrastructure.MySql/Migrations/20240216120800_Reset_LocalLibraryFolders.Designer.cs generated

File diff suppressed because it is too large Load Diff

31
ErsatzTV.Infrastructure.MySql/Migrations/20240216120800_Reset_LocalLibraryFolders.cs

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Reset_LocalLibraryFolders : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// reset local library folders, and require scans
migrationBuilder.Sql(
@"UPDATE LibraryPath SET LastScan = '0001-01-01 00:00:00' WHERE Id IN
(SELECT LP.Id FROM Library L INNER JOIN LibraryPath LP ON L.Id = LP.LibraryId INNER JOIN LocalMediaSource LMS ON LMS.Id = L.MediaSourceId)");
migrationBuilder.Sql(
@"UPDATE Library SET LastScan = '0001-01-01 00:00:00' WHERE MediaSourceId IN (SELECT Id FROM LocalMediaSource)");
migrationBuilder.Sql(
@"DELETE FROM LibraryFolder WHERE LibraryPathId IN (SELECT LP.Id FROM Library L INNER JOIN LibraryPath LP ON L.Id = LP.LibraryId INNER JOIN LocalMediaSource LMS ON LMS.Id = L.MediaSourceId)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

31
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -904,6 +904,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -904,6 +904,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("LibraryPathId")
.HasColumnType("int");
b.Property<int?>("ParentId")
.HasColumnType("int");
b.Property<string>("Path")
.HasColumnType("longtext");
@ -911,6 +914,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -911,6 +914,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.HasIndex("LibraryPathId");
b.HasIndex("ParentId");
b.ToTable("LibraryFolder", (string)null);
});
@ -970,6 +975,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -970,6 +975,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("LibraryFolderId")
.HasColumnType("int");
b.Property<int>("MediaVersionId")
.HasColumnType("int");
@ -978,6 +986,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -978,6 +986,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.HasKey("Id");
b.HasIndex("LibraryFolderId");
b.HasIndex("MediaVersionId");
b.HasIndex("Path")
@ -3470,7 +3480,14 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -3470,7 +3480,14 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.LibraryFolder", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("LibraryPath");
b.Navigation("Parent");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b =>
@ -3497,12 +3514,19 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -3497,12 +3514,19 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b =>
{
b.HasOne("ErsatzTV.Core.Domain.LibraryFolder", "LibraryFolder")
.WithMany("MediaFiles")
.HasForeignKey("LibraryFolderId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion")
.WithMany("MediaFiles")
.HasForeignKey("MediaVersionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("LibraryFolder");
b.Navigation("MediaVersion");
});
@ -4789,6 +4813,13 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4789,6 +4813,13 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("Paths");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryFolder", b =>
{
b.Navigation("Children");
b.Navigation("MediaFiles");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b =>
{
b.Navigation("LibraryFolders");

5134
ErsatzTV.Infrastructure.Sqlite/Migrations/20240216022703_Rework_LibraryFolder.Designer.cs generated

File diff suppressed because it is too large Load Diff

80
ErsatzTV.Infrastructure.Sqlite/Migrations/20240216022703_Rework_LibraryFolder.cs

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Rework_LibraryFolder : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "LibraryFolderId",
table: "MediaFile",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "ParentId",
table: "LibraryFolder",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_MediaFile_LibraryFolderId",
table: "MediaFile",
column: "LibraryFolderId");
migrationBuilder.CreateIndex(
name: "IX_LibraryFolder_ParentId",
table: "LibraryFolder",
column: "ParentId");
migrationBuilder.AddForeignKey(
name: "FK_LibraryFolder_LibraryFolder_ParentId",
table: "LibraryFolder",
column: "ParentId",
principalTable: "LibraryFolder",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_MediaFile_LibraryFolder_LibraryFolderId",
table: "MediaFile",
column: "LibraryFolderId",
principalTable: "LibraryFolder",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_LibraryFolder_LibraryFolder_ParentId",
table: "LibraryFolder");
migrationBuilder.DropForeignKey(
name: "FK_MediaFile_LibraryFolder_LibraryFolderId",
table: "MediaFile");
migrationBuilder.DropIndex(
name: "IX_MediaFile_LibraryFolderId",
table: "MediaFile");
migrationBuilder.DropIndex(
name: "IX_LibraryFolder_ParentId",
table: "LibraryFolder");
migrationBuilder.DropColumn(
name: "LibraryFolderId",
table: "MediaFile");
migrationBuilder.DropColumn(
name: "ParentId",
table: "LibraryFolder");
}
}
}

5134
ErsatzTV.Infrastructure.Sqlite/Migrations/20240216022831_Reset_LocalLibraryFolders.Designer.cs generated

File diff suppressed because it is too large Load Diff

31
ErsatzTV.Infrastructure.Sqlite/Migrations/20240216022831_Reset_LocalLibraryFolders.cs

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Reset_LocalLibraryFolders : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// reset local library folders, and require scans
migrationBuilder.Sql(
@"UPDATE LibraryPath SET LastScan = '0001-01-01 00:00:00' WHERE Id IN
(SELECT LP.Id FROM Library L INNER JOIN LibraryPath LP ON L.Id = LP.LibraryId INNER JOIN LocalMediaSource LMS ON LMS.Id = L.MediaSourceId)");
migrationBuilder.Sql(
@"UPDATE Library SET LastScan = '0001-01-01 00:00:00' WHERE MediaSourceId IN (SELECT Id FROM LocalMediaSource)");
migrationBuilder.Sql(
@"DELETE FROM LibraryFolder WHERE LibraryPathId IN (SELECT LP.Id FROM Library L INNER JOIN LibraryPath LP ON L.Id = LP.LibraryId INNER JOIN LocalMediaSource LMS ON LMS.Id = L.MediaSourceId)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

31
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -902,6 +902,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -902,6 +902,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("LibraryPathId")
.HasColumnType("INTEGER");
b.Property<int?>("ParentId")
.HasColumnType("INTEGER");
b.Property<string>("Path")
.HasColumnType("TEXT");
@ -909,6 +912,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -909,6 +912,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("LibraryPathId");
b.HasIndex("ParentId");
b.ToTable("LibraryFolder", (string)null);
});
@ -968,6 +973,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -968,6 +973,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("LibraryFolderId")
.HasColumnType("INTEGER");
b.Property<int>("MediaVersionId")
.HasColumnType("INTEGER");
@ -976,6 +984,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -976,6 +984,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasKey("Id");
b.HasIndex("LibraryFolderId");
b.HasIndex("MediaVersionId");
b.HasIndex("Path")
@ -3468,7 +3478,14 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -3468,7 +3478,14 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.LibraryFolder", "Parent")
.WithMany("Children")
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("LibraryPath");
b.Navigation("Parent");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b =>
@ -3495,12 +3512,19 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -3495,12 +3512,19 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b =>
{
b.HasOne("ErsatzTV.Core.Domain.LibraryFolder", "LibraryFolder")
.WithMany("MediaFiles")
.HasForeignKey("LibraryFolderId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion")
.WithMany("MediaFiles")
.HasForeignKey("MediaVersionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("LibraryFolder");
b.Navigation("MediaVersion");
});
@ -4787,6 +4811,13 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4787,6 +4811,13 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("Paths");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryFolder", b =>
{
b.Navigation("Children");
b.Navigation("MediaFiles");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b =>
{
b.Navigation("LibraryFolders");

11
ErsatzTV.Infrastructure/Data/Configurations/Library/LibraryFolderConfiguration.cs

@ -6,5 +6,14 @@ namespace ErsatzTV.Infrastructure.Data.Configurations; @@ -6,5 +6,14 @@ namespace ErsatzTV.Infrastructure.Data.Configurations;
public class LibraryFolderConfiguration : IEntityTypeConfiguration<LibraryFolder>
{
public void Configure(EntityTypeBuilder<LibraryFolder> builder) => builder.ToTable("LibraryFolder");
public void Configure(EntityTypeBuilder<LibraryFolder> builder)
{
builder.ToTable("LibraryFolder");
builder.HasOne(f => f.Parent)
.WithMany(p => p.Children)
.HasForeignKey(f => f.ParentId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
}
}

6
ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MediaFileConfiguration.cs

@ -12,5 +12,11 @@ public class MediaFileConfiguration : IEntityTypeConfiguration<MediaFile> @@ -12,5 +12,11 @@ public class MediaFileConfiguration : IEntityTypeConfiguration<MediaFile>
builder.HasIndex(f => f.Path)
.IsUnique();
builder.HasOne(f => f.LibraryFolder)
.WithMany(f => f.MediaFiles)
.HasForeignKey(f => f.LibraryFolderId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
}
}

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

@ -22,6 +22,7 @@ public class ImageRepository : IImageRepository @@ -22,6 +22,7 @@ public class ImageRepository : IImageRepository
public async Task<Either<BaseError, MediaItemScanResult<Image>>> GetOrAdd(
LibraryPath libraryPath,
LibraryFolder libraryFolder,
string path)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -55,7 +56,7 @@ public class ImageRepository : IImageRepository @@ -55,7 +56,7 @@ public class ImageRepository : IImageRepository
mediaItem =>
Right<BaseError, MediaItemScanResult<Image>>(
new MediaItemScanResult<Image>(mediaItem) { IsAdded = false }).AsTask(),
async () => await AddImage(dbContext, libraryPath.Id, path));
async () => await AddImage(dbContext, libraryPath.Id, libraryFolder.Id, path));
}
public async Task<IEnumerable<string>> FindImagePaths(LibraryPath libraryPath)
@ -124,6 +125,7 @@ public class ImageRepository : IImageRepository @@ -124,6 +125,7 @@ public class ImageRepository : IImageRepository
private async Task<Either<BaseError, MediaItemScanResult<Image>>> AddImage(
TvContext dbContext,
int libraryPathId,
int libraryFolderId,
string path)
{
try
@ -133,14 +135,14 @@ public class ImageRepository : IImageRepository @@ -133,14 +135,14 @@ public class ImageRepository : IImageRepository
return new MediaFileAlreadyExists();
}
var otherVideo = new Image
var image = new Image
{
LibraryPathId = libraryPathId,
MediaVersions =
[
new MediaVersion
{
MediaFiles = [new MediaFile { Path = path }],
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
Streams = []
}
],
@ -150,11 +152,11 @@ public class ImageRepository : IImageRepository @@ -150,11 +152,11 @@ public class ImageRepository : IImageRepository
}
};
await dbContext.Images.AddAsync(otherVideo);
await dbContext.Images.AddAsync(image);
await dbContext.SaveChangesAsync();
await dbContext.Entry(otherVideo).Reference(m => m.LibraryPath).LoadAsync();
await dbContext.Entry(otherVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync();
return new MediaItemScanResult<Image>(otherVideo) { IsAdded = true };
await dbContext.Entry(image).Reference(m => m.LibraryPath).LoadAsync();
await dbContext.Entry(image.LibraryPath).Reference(lp => lp.Library).LoadAsync();
return new MediaItemScanResult<Image>(image) { IsAdded = true };
}
catch (Exception ex)
{

152
ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Infrastructure.Data.Repositories;
@ -90,49 +91,136 @@ public class LibraryRepository : ILibraryRepository @@ -90,49 +91,136 @@ public class LibraryRepository : ILibraryRepository
new { LibraryPathId = libraryPathId });
}
public async Task<Unit> SetEtag(
public async Task SetEtag(
LibraryPath libraryPath,
Option<LibraryFolder> knownFolder,
string path,
string etag) =>
await knownFolder.Match(
async folder =>
string etag)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
foreach (LibraryFolder folder in knownFolder)
{
await dbContext.Connection.ExecuteAsync(
"UPDATE LibraryFolder SET Etag = @Etag WHERE Id = @Id",
new { folder.Id, Etag = etag });
}
if (knownFolder.IsNone)
{
await dbContext.LibraryFolders.AddAsync(
new LibraryFolder
{
Path = path,
Etag = etag,
LibraryPathId = libraryPath.Id
});
await dbContext.SaveChangesAsync();
}
}
public async Task CleanEtagsForLibraryPath(LibraryPath libraryPath)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
IOrderedEnumerable<LibraryFolder> orderedFolders = libraryPath.LibraryFolders
.Where(f => !_localFileSystem.FolderExists(f.Path))
.OrderByDescending(lp => lp.Path.Length);
foreach (LibraryFolder folder in orderedFolders)
{
await dbContext.Connection.ExecuteAsync(
"""
DELETE FROM LibraryFolder WHERE Id = @LibraryFolderId
AND NOT EXISTS (SELECT Id FROM MediaFile WHERE LibraryFolderId = @LibraryFolderId)
AND NOT EXISTS (SELECT Id FROM LibraryFolder WHERE ParentId = @LibraryFolderId)
""",
new { LibraryFolderId = folder.Id });
}
}
public async Task<Option<int>> GetParentFolderId(string folder)
{
DirectoryInfo parent = new DirectoryInfo(folder).Parent;
if (parent is null)
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.LibraryFolders
.AsNoTracking()
.SelectOneAsync(lf => lf.Path, lf => lf.Path == parent.FullName)
.MapT(lf => lf.Id);
}
public async Task<LibraryFolder> GetOrAddFolder(LibraryPath libraryPath, Option<int> maybeParentFolder, string folder)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
// load from db or create new folder
LibraryFolder knownFolder = await libraryPath.LibraryFolders
.Filter(f => f.Path == folder)
.HeadOrNone()
.IfNoneAsync(CreateNewFolder(libraryPath, maybeParentFolder, folder));
// update parent folder if not present
foreach (int parentFolder in maybeParentFolder)
{
if (knownFolder.ParentId != parentFolder)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
knownFolder.ParentId = parentFolder;
await dbContext.Connection.ExecuteAsync(
"UPDATE LibraryFolder SET Etag = @Etag WHERE Id = @Id",
new { folder.Id, Etag = etag });
},
async () =>
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
await dbContext.LibraryFolders.AddAsync(
new LibraryFolder
{
Path = path,
Etag = etag,
LibraryPathId = libraryPath.Id
});
await dbContext.SaveChangesAsync();
}).ToUnit();
public async Task<Unit> CleanEtagsForLibraryPath(LibraryPath libraryPath)
"UPDATE LibraryFolder SET ParentId = @ParentId WHERE Id = @Id",
new { ParentId = parentFolder, knownFolder.Id });
}
}
// add new folder to library path
if (knownFolder.Id < 1)
{
await dbContext.LibraryFolders.AddAsync(knownFolder);
await dbContext.SaveChangesAsync();
}
return knownFolder;
}
public async Task UpdateLibraryFolderId(MediaFile mediaFile, int libraryFolderId)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
mediaFile.LibraryFolderId = libraryFolderId;
await dbContext.Connection.ExecuteAsync(
"UPDATE MediaFile SET LibraryFolderId = @LibraryFolderId WHERE Id = @Id",
new { LibraryFolderId = libraryFolderId, mediaFile.Id });
}
IEnumerable<string> folders = await dbContext.Connection.QueryAsync<string>(
@"SELECT LF.Path
FROM LibraryFolder LF
WHERE LF.LibraryPathId = @LibraryPathId",
new { LibraryPathId = libraryPath.Id });
public async Task UpdatePath(LibraryPath libraryPath, string normalizedLibraryPath)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
libraryPath.Path = normalizedLibraryPath;
await dbContext.Connection.ExecuteAsync(
"UPDATE LibraryPath SET Path = @Path WHERE Id = @Id",
new { Path = normalizedLibraryPath, libraryPath.Id });
}
foreach (string folder in folders.Where(f => !_localFileSystem.FolderExists(f)))
private static LibraryFolder CreateNewFolder(LibraryPath libraryPath, Option<int> maybeParentFolder, string folder)
{
int? parentId = null;
foreach (int parentFolder in maybeParentFolder)
{
await dbContext.Connection.ExecuteAsync(
@"DELETE FROM LibraryFolder WHERE LibraryPathId = @LibraryPathId AND Path = @Path",
new { LibraryPathId = libraryPath.Id, Path = folder });
parentId = parentFolder;
}
return Unit.Default;
return new LibraryFolder
{
Path = folder,
Etag = null,
LibraryPathId = libraryPath.Id,
ParentId = parentId
};
}
}

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

@ -57,7 +57,10 @@ public class MovieRepository : IMovieRepository @@ -57,7 +57,10 @@ public class MovieRepository : IMovieRepository
.Map(Optional);
}
public async Task<Either<BaseError, MediaItemScanResult<Movie>>> GetOrAdd(LibraryPath libraryPath, string path)
public async Task<Either<BaseError, MediaItemScanResult<Movie>>> GetOrAdd(
LibraryPath libraryPath,
LibraryFolder libraryFolder,
string path)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<Movie> maybeExisting = await dbContext.Movies
@ -96,7 +99,7 @@ public class MovieRepository : IMovieRepository @@ -96,7 +99,7 @@ public class MovieRepository : IMovieRepository
mediaItem =>
Right<BaseError, MediaItemScanResult<Movie>>(
new MediaItemScanResult<Movie>(mediaItem) { IsAdded = false }).AsTask(),
async () => await AddMovie(dbContext, libraryPath.Id, path));
async () => await AddMovie(dbContext, libraryPath.Id, libraryFolder.Id, path));
}
public async Task<List<MovieMetadata>> GetMoviesForCards(List<int> ids)
@ -232,6 +235,7 @@ public class MovieRepository : IMovieRepository @@ -232,6 +235,7 @@ public class MovieRepository : IMovieRepository
private async Task<Either<BaseError, MediaItemScanResult<Movie>>> AddMovie(
TvContext dbContext,
int libraryPathId,
int libraryFolderId,
string path)
{
try
@ -244,18 +248,15 @@ public class MovieRepository : IMovieRepository @@ -244,18 +248,15 @@ public class MovieRepository : IMovieRepository
var movie = new Movie
{
LibraryPathId = libraryPathId,
MediaVersions = new List<MediaVersion>
{
new()
MediaVersions =
[
new MediaVersion
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
},
Streams = new List<MediaStream>()
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
Streams = []
}
},
TraktListItems = new List<TraktListItem>()
],
TraktListItems = []
};
await dbContext.Movies.AddAsync(movie);
await dbContext.SaveChangesAsync();

24
ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs

@ -16,6 +16,7 @@ public class MusicVideoRepository : IMusicVideoRepository @@ -16,6 +16,7 @@ public class MusicVideoRepository : IMusicVideoRepository
public async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> GetOrAdd(
Artist artist,
LibraryPath libraryPath,
LibraryFolder libraryFolder,
string path)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -62,7 +63,7 @@ public class MusicVideoRepository : IMusicVideoRepository @@ -62,7 +63,7 @@ public class MusicVideoRepository : IMusicVideoRepository
return Right<BaseError, MediaItemScanResult<MusicVideo>>(
new MediaItemScanResult<MusicVideo>(mediaItem) { IsAdded = false });
},
async () => await AddMusicVideo(dbContext, artist, libraryPath.Id, path));
async () => await AddMusicVideo(dbContext, artist, libraryPath.Id, libraryFolder.Id, path));
}
public async Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath)
@ -219,6 +220,7 @@ public class MusicVideoRepository : IMusicVideoRepository @@ -219,6 +220,7 @@ public class MusicVideoRepository : IMusicVideoRepository
TvContext dbContext,
Artist artist,
int libraryPathId,
int libraryFolderId,
string path)
{
try
@ -227,18 +229,18 @@ public class MusicVideoRepository : IMusicVideoRepository @@ -227,18 +229,18 @@ public class MusicVideoRepository : IMusicVideoRepository
{
ArtistId = artist.Id,
LibraryPathId = libraryPathId,
MediaVersions = new List<MediaVersion>
{
new()
MediaVersions =
[
new MediaVersion
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
},
Streams = new List<MediaStream>()
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
Streams = []
}
},
TraktListItems = new List<TraktListItem>()
],
TraktListItems = new List<TraktListItem>
{
Capacity = 0
}
};
await dbContext.MusicVideos.AddAsync(musicVideo);

21
ErsatzTV.Infrastructure/Data/Repositories/OtherVideoRepository.cs

@ -22,6 +22,7 @@ public class OtherVideoRepository : IOtherVideoRepository @@ -22,6 +22,7 @@ public class OtherVideoRepository : IOtherVideoRepository
public async Task<Either<BaseError, MediaItemScanResult<OtherVideo>>> GetOrAdd(
LibraryPath libraryPath,
LibraryFolder libraryFolder,
string path)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -59,7 +60,7 @@ public class OtherVideoRepository : IOtherVideoRepository @@ -59,7 +60,7 @@ public class OtherVideoRepository : IOtherVideoRepository
mediaItem =>
Right<BaseError, MediaItemScanResult<OtherVideo>>(
new MediaItemScanResult<OtherVideo>(mediaItem) { IsAdded = false }).AsTask(),
async () => await AddOtherVideo(dbContext, libraryPath.Id, path));
async () => await AddOtherVideo(dbContext, libraryPath.Id, libraryFolder.Id, path));
}
public async Task<IEnumerable<string>> FindOtherVideoPaths(LibraryPath libraryPath)
@ -189,6 +190,7 @@ public class OtherVideoRepository : IOtherVideoRepository @@ -189,6 +190,7 @@ public class OtherVideoRepository : IOtherVideoRepository
private async Task<Either<BaseError, MediaItemScanResult<OtherVideo>>> AddOtherVideo(
TvContext dbContext,
int libraryPathId,
int libraryFolderId,
string path)
{
try
@ -201,18 +203,15 @@ public class OtherVideoRepository : IOtherVideoRepository @@ -201,18 +203,15 @@ public class OtherVideoRepository : IOtherVideoRepository
var otherVideo = new OtherVideo
{
LibraryPathId = libraryPathId,
MediaVersions = new List<MediaVersion>
{
new()
MediaVersions =
[
new MediaVersion
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
},
Streams = new List<MediaStream>()
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
Streams = []
}
},
TraktListItems = new List<TraktListItem>()
],
TraktListItems = []
};
await dbContext.OtherVideos.AddAsync(otherVideo);

21
ErsatzTV.Infrastructure/Data/Repositories/SongRepository.cs

@ -15,6 +15,7 @@ public class SongRepository : ISongRepository @@ -15,6 +15,7 @@ public class SongRepository : ISongRepository
public async Task<Either<BaseError, MediaItemScanResult<Song>>> GetOrAdd(
LibraryPath libraryPath,
LibraryFolder libraryFolder,
string path)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -44,7 +45,7 @@ public class SongRepository : ISongRepository @@ -44,7 +45,7 @@ public class SongRepository : ISongRepository
mediaItem =>
Right<BaseError, MediaItemScanResult<Song>>(
new MediaItemScanResult<Song>(mediaItem) { IsAdded = false }).AsTask(),
async () => await AddSong(dbContext, libraryPath.Id, path));
async () => await AddSong(dbContext, libraryPath.Id, libraryFolder.Id, path));
}
public async Task<IEnumerable<string>> FindSongPaths(LibraryPath libraryPath)
@ -121,6 +122,7 @@ public class SongRepository : ISongRepository @@ -121,6 +122,7 @@ public class SongRepository : ISongRepository
private static async Task<Either<BaseError, MediaItemScanResult<Song>>> AddSong(
TvContext dbContext,
int libraryPathId,
int libraryFolderId,
string path)
{
try
@ -128,18 +130,15 @@ public class SongRepository : ISongRepository @@ -128,18 +130,15 @@ public class SongRepository : ISongRepository
var song = new Song
{
LibraryPathId = libraryPathId,
MediaVersions = new List<MediaVersion>
{
new()
MediaVersions =
[
new MediaVersion
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
},
Streams = new List<MediaStream>()
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
Streams = []
}
},
TraktListItems = new List<TraktListItem>()
],
TraktListItems = []
};
await dbContext.Songs.AddAsync(song);

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

@ -356,6 +356,7 @@ public class TelevisionRepository : ITelevisionRepository @@ -356,6 +356,7 @@ public class TelevisionRepository : ITelevisionRepository
public async Task<Either<BaseError, Episode>> GetOrAddEpisode(
Season season,
LibraryPath libraryPath,
LibraryFolder libraryFolder,
string path)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
@ -407,7 +408,7 @@ public class TelevisionRepository : ITelevisionRepository @@ -407,7 +408,7 @@ public class TelevisionRepository : ITelevisionRepository
return episode;
},
async () => await AddEpisode(dbContext, season, libraryPath.Id, path));
async () => await AddEpisode(dbContext, season, libraryPath.Id, libraryFolder.Id, path));
}
public async Task<IEnumerable<string>> FindEpisodePaths(LibraryPath libraryPath)
@ -723,6 +724,7 @@ public class TelevisionRepository : ITelevisionRepository @@ -723,6 +724,7 @@ public class TelevisionRepository : ITelevisionRepository
TvContext dbContext,
Season season,
int libraryPathId,
int libraryFolderId,
string path)
{
try
@ -736,34 +738,31 @@ public class TelevisionRepository : ITelevisionRepository @@ -736,34 +738,31 @@ public class TelevisionRepository : ITelevisionRepository
{
LibraryPathId = libraryPathId,
SeasonId = season.Id,
EpisodeMetadata = new List<EpisodeMetadata>
{
new()
EpisodeMetadata =
[
new EpisodeMetadata
{
DateAdded = DateTime.UtcNow,
DateUpdated = SystemTime.MinValueUtc,
MetadataKind = MetadataKind.Fallback,
Actors = new List<Actor>(),
Guids = new List<MetadataGuid>(),
Writers = new List<Writer>(),
Directors = new List<Director>(),
Genres = new List<Genre>(),
Tags = new List<Tag>(),
Studios = new List<Studio>()
Actors = [],
Guids = [],
Writers = [],
Directors = [],
Genres = [],
Tags = [],
Studios = []
}
},
MediaVersions = new List<MediaVersion>
{
new()
],
MediaVersions =
[
new MediaVersion
{
MediaFiles = new List<MediaFile>
{
new() { Path = path }
},
Streams = new List<MediaStream>()
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }],
Streams = []
}
},
TraktListItems = new List<TraktListItem>()
],
TraktListItems = []
};
await dbContext.Episodes.AddAsync(episode);
await dbContext.SaveChangesAsync();

55
ErsatzTV.Scanner.Tests/Core/Metadata/MovieFolderScannerTests.cs

@ -45,7 +45,7 @@ public class MovieFolderScannerTests @@ -45,7 +45,7 @@ public class MovieFolderScannerTests
public void SetUp()
{
_movieRepository = Substitute.For<IMovieRepository>();
_movieRepository.GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<string>())
_movieRepository.GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<LibraryFolder>(), Arg.Any<string>())
.Returns(
args =>
Right<BaseError, MediaItemScanResult<Movie>>(new FakeMovieWithPath(args.Arg<string>()))
@ -73,6 +73,10 @@ public class MovieFolderScannerTests @@ -73,6 +73,10 @@ public class MovieFolderScannerTests
});
_imageCache = Substitute.For<IImageCache>();
_libraryRepository = Substitute.For<ILibraryRepository>();
_libraryRepository.GetOrAddFolder(Arg.Any<LibraryPath>(), Arg.Any<Option<int>>(), Arg.Any<string>())
.Returns(new LibraryFolder());
}
private IMovieRepository _movieRepository;
@ -80,6 +84,7 @@ public class MovieFolderScannerTests @@ -80,6 +84,7 @@ public class MovieFolderScannerTests
private ILocalStatisticsProvider _localStatisticsProvider;
private ILocalMetadataProvider _localMetadataProvider;
private IImageCache _imageCache;
private ILibraryRepository _libraryRepository;
[Test]
public async Task NewMovie_Statistics_And_FallbackMetadata(
@ -106,8 +111,8 @@ public class MovieFolderScannerTests @@ -106,8 +111,8 @@ public class MovieFolderScannerTests
result.IsRight.Should().BeTrue();
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, moviePath);
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<LibraryFolder>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, Arg.Any<LibraryFolder>(), moviePath);
await _localStatisticsProvider.Received(1).RefreshStatistics(
FFmpegPath,
@ -147,8 +152,8 @@ public class MovieFolderScannerTests @@ -147,8 +152,8 @@ public class MovieFolderScannerTests
result.IsRight.Should().BeTrue();
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, moviePath);
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<LibraryFolder>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, Arg.Any<LibraryFolder>(), moviePath);
await _localStatisticsProvider.Received(1).RefreshStatistics(
FFmpegPath,
@ -187,8 +192,8 @@ public class MovieFolderScannerTests @@ -187,8 +192,8 @@ public class MovieFolderScannerTests
result.IsRight.Should().BeTrue();
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, moviePath);
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<LibraryFolder>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, Arg.Any<LibraryFolder>(), moviePath);
await _localStatisticsProvider.Received(1).RefreshStatistics(
FFmpegPath,
@ -228,8 +233,8 @@ public class MovieFolderScannerTests @@ -228,8 +233,8 @@ public class MovieFolderScannerTests
result.IsRight.Should().BeTrue();
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, moviePath);
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<LibraryFolder>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, Arg.Any<LibraryFolder>(), moviePath);
await _localStatisticsProvider.Received(1).RefreshStatistics(
FFmpegPath,
@ -273,8 +278,8 @@ public class MovieFolderScannerTests @@ -273,8 +278,8 @@ public class MovieFolderScannerTests
result.IsRight.Should().BeTrue();
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, moviePath);
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<LibraryFolder>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, Arg.Any<LibraryFolder>(), moviePath);
await _localStatisticsProvider.Received(1).RefreshStatistics(
FFmpegPath,
@ -319,8 +324,8 @@ public class MovieFolderScannerTests @@ -319,8 +324,8 @@ public class MovieFolderScannerTests
result.IsRight.Should().BeTrue();
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, moviePath);
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<LibraryFolder>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, Arg.Any<LibraryFolder>(), moviePath);
await _localStatisticsProvider.Received(1).RefreshStatistics(
FFmpegPath,
@ -365,8 +370,8 @@ public class MovieFolderScannerTests @@ -365,8 +370,8 @@ public class MovieFolderScannerTests
result.IsRight.Should().BeTrue();
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, moviePath);
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<LibraryFolder>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, Arg.Any<LibraryFolder>(), moviePath);
await _localStatisticsProvider.Received(1).RefreshStatistics(
FFmpegPath,
@ -410,8 +415,8 @@ public class MovieFolderScannerTests @@ -410,8 +415,8 @@ public class MovieFolderScannerTests
result.IsRight.Should().BeTrue();
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, moviePath);
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<LibraryFolder>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, Arg.Any<LibraryFolder>(), moviePath);
await _localStatisticsProvider.Received(1).RefreshStatistics(
FFmpegPath,
@ -451,8 +456,8 @@ public class MovieFolderScannerTests @@ -451,8 +456,8 @@ public class MovieFolderScannerTests
result.IsRight.Should().BeTrue();
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, moviePath);
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<LibraryFolder>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, Arg.Any<LibraryFolder>(), moviePath);
await _localStatisticsProvider.Received(1).RefreshStatistics(
FFmpegPath,
@ -494,8 +499,8 @@ public class MovieFolderScannerTests @@ -494,8 +499,8 @@ public class MovieFolderScannerTests
result.IsRight.Should().BeTrue();
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, moviePath);
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<LibraryFolder>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, Arg.Any<LibraryFolder>(), moviePath);
await _localStatisticsProvider.Received(1).RefreshStatistics(
FFmpegPath,
@ -531,8 +536,8 @@ public class MovieFolderScannerTests @@ -531,8 +536,8 @@ public class MovieFolderScannerTests
result.IsRight.Should().BeTrue();
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, moviePath);
await _movieRepository.Received(1).GetOrAdd(Arg.Any<LibraryPath>(), Arg.Any<LibraryFolder>(), Arg.Any<string>());
await _movieRepository.Received(1).GetOrAdd(libraryPath, Arg.Any<LibraryFolder>(), moviePath);
await _localStatisticsProvider.Received(1).RefreshStatistics(
FFmpegPath,
@ -615,7 +620,7 @@ public class MovieFolderScannerTests @@ -615,7 +620,7 @@ public class MovieFolderScannerTests
_localMetadataProvider,
Substitute.For<IMetadataRepository>(),
_imageCache,
Substitute.For<ILibraryRepository>(),
_libraryRepository,
_mediaItemRepository,
Substitute.For<IMediator>(),
Substitute.For<IFFmpegPngService>(),
@ -633,7 +638,7 @@ public class MovieFolderScannerTests @@ -633,7 +638,7 @@ public class MovieFolderScannerTests
_localMetadataProvider,
Substitute.For<IMetadataRepository>(),
_imageCache,
Substitute.For<ILibraryRepository>(),
_libraryRepository,
_mediaItemRepository,
Substitute.For<IMediator>(),
Substitute.For<IFFmpegPngService>(),

42
ErsatzTV.Scanner/Core/Metadata/ImageFolderScanner.cs

@ -75,6 +75,14 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner @@ -75,6 +75,14 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
var allFolders = new System.Collections.Generic.HashSet<string>();
var folderQueue = new Queue<string>();
string normalizedLibraryPath = libraryPath.Path.TrimEnd(
Path.DirectorySeparatorChar,
Path.AltDirectorySeparatorChar);
if (libraryPath.Path != normalizedLibraryPath)
{
await _libraryRepository.UpdatePath(libraryPath, normalizedLibraryPath);
}
if (ShouldIncludeFolder(libraryPath.Path) && allFolders.Add(libraryPath.Path))
{
folderQueue.Enqueue(libraryPath.Path);
@ -106,6 +114,8 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner @@ -106,6 +114,8 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
cancellationToken);
string imageFolder = folderQueue.Dequeue();
Option<int> maybeParentFolder = await _libraryRepository.GetParentFolderId(imageFolder);
foldersCompleted++;
var filesForEtag = _localFileSystem.ListFiles(imageFolder).ToList();
@ -124,29 +134,27 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner @@ -124,29 +134,27 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
}
string etag = FolderEtag.Calculate(imageFolder, _localFileSystem);
Option<LibraryFolder> knownFolder = libraryPath.LibraryFolders
.Filter(f => f.Path == imageFolder)
.HeadOrNone();
LibraryFolder knownFolder = await _libraryRepository.GetOrAddFolder(
libraryPath,
maybeParentFolder,
imageFolder);
// skip folder if etag matches
if (allFiles.Count == 0 ||
await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) ==
etag)
if (allFiles.Count == 0 || knownFolder.Etag == etag)
{
continue;
}
_logger.LogDebug(
"UPDATE: Etag has changed for folder {Folder}",
imageFolder);
_logger.LogDebug("UPDATE: Etag has changed for folder {Folder}", imageFolder);
var hasErrors = false;
foreach (string file in allFiles.OrderBy(identity))
{
Either<BaseError, MediaItemScanResult<Image>> maybeVideo = await _imageRepository
.GetOrAdd(libraryPath, file)
.GetOrAdd(libraryPath, knownFolder, file)
.BindT(video => UpdateStatistics(video, ffmpegPath, ffprobePath))
.BindT(video => UpdateLibraryFolderId(video, knownFolder))
.BindT(UpdateMetadata)
//.BindT(video => UpdateThumbnail(video, cancellationToken))
//.BindT(UpdateSubtitles)
@ -221,6 +229,20 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner @@ -221,6 +229,20 @@ public class ImageFolderScanner : LocalFolderScanner, IImageFolderScanner
}
}
private async Task<Either<BaseError, MediaItemScanResult<Image>>> UpdateLibraryFolderId(
MediaItemScanResult<Image> video,
LibraryFolder libraryFolder)
{
MediaFile mediaFile = video.Item.GetHeadVersion().MediaFiles.Head();
if (mediaFile.LibraryFolderId != libraryFolder.Id)
{
await _libraryRepository.UpdateLibraryFolderId(mediaFile, libraryFolder.Id);
video.IsUpdated = true;
}
return video;
}
private async Task<Either<BaseError, MediaItemScanResult<Image>>> UpdateMetadata(
MediaItemScanResult<Image> result)
{

43
ErsatzTV.Scanner/Core/Metadata/MovieFolderScanner.cs

@ -3,6 +3,7 @@ using Bugsnag; @@ -3,6 +3,7 @@ using Bugsnag;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
@ -82,6 +83,15 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -82,6 +83,15 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
var foldersCompleted = 0;
var folderQueue = new Queue<string>();
string normalizedLibraryPath = libraryPath.Path.TrimEnd(
Path.DirectorySeparatorChar,
Path.AltDirectorySeparatorChar);
if (libraryPath.Path != normalizedLibraryPath)
{
await _libraryRepository.UpdatePath(libraryPath, normalizedLibraryPath);
}
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path)
.Filter(ShouldIncludeFolder)
.OrderBy(identity))
@ -107,6 +117,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -107,6 +117,7 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
cancellationToken);
string movieFolder = folderQueue.Dequeue();
Option<int> maybeParentFolder = await _libraryRepository.GetParentFolderId(movieFolder);
foldersCompleted++;
var filesForEtag = _localFileSystem.ListFiles(movieFolder).ToList();
@ -120,9 +131,10 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -120,9 +131,10 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
.ToList();
string etag = FolderEtag.Calculate(movieFolder, _localFileSystem);
Option<LibraryFolder> knownFolder = libraryPath.LibraryFolders
.Filter(f => f.Path == movieFolder)
.HeadOrNone();
LibraryFolder knownFolder = await _libraryRepository.GetOrAddFolder(
libraryPath,
maybeParentFolder,
movieFolder);
if (allFiles.Count == 0)
{
@ -134,16 +146,12 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -134,16 +146,12 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
}
// store etag for now-empty folders
if (knownFolder.IsSome)
{
await _libraryRepository.SetEtag(libraryPath, knownFolder, movieFolder, etag);
}
await _libraryRepository.SetEtag(libraryPath, knownFolder, movieFolder, etag);
continue;
}
bool etagMatches = await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag;
if (etagMatches)
if (knownFolder.Etag == etag)
{
if (allFiles.Any(allTrashedItems.Contains))
{
@ -166,8 +174,9 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -166,8 +174,9 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
{
// TODO: figure out how to rebuild playlists
Either<BaseError, MediaItemScanResult<Movie>> maybeMovie = await _movieRepository
.GetOrAdd(libraryPath, file)
.GetOrAdd(libraryPath, knownFolder, file)
.BindT(movie => UpdateStatistics(movie, ffmpegPath, ffprobePath))
.BindT(video => UpdateLibraryFolderId(video, knownFolder))
.BindT(UpdateMetadata)
.BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster, cancellationToken))
.BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt, cancellationToken))
@ -227,6 +236,20 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner @@ -227,6 +236,20 @@ public class MovieFolderScanner : LocalFolderScanner, IMovieFolderScanner
}
}
private async Task<Either<BaseError, MediaItemScanResult<Movie>>> UpdateLibraryFolderId(
MediaItemScanResult<Movie> video,
LibraryFolder libraryFolder)
{
MediaFile mediaFile = video.Item.GetHeadVersion().MediaFiles.Head();
if (mediaFile.LibraryFolderId != libraryFolder.Id)
{
await _libraryRepository.UpdateLibraryFolderId(mediaFile, libraryFolder.Id);
video.IsUpdated = true;
}
return video;
}
private async Task<Either<BaseError, MediaItemScanResult<Movie>>> UpdateMetadata(
MediaItemScanResult<Movie> result)
{

40
ErsatzTV.Scanner/Core/Metadata/MusicVideoFolderScanner.cs

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
@ -75,6 +76,14 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -75,6 +76,14 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
{
decimal progressSpread = progressMax - progressMin;
string normalizedLibraryPath = libraryPath.Path.TrimEnd(
Path.DirectorySeparatorChar,
Path.AltDirectorySeparatorChar);
if (libraryPath.Path != normalizedLibraryPath)
{
await _libraryRepository.UpdatePath(libraryPath, normalizedLibraryPath);
}
var allArtistFolders = _localFileSystem.ListSubdirectories(libraryPath.Path)
.Filter(ShouldIncludeFolder)
.OrderBy(identity)
@ -316,6 +325,8 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -316,6 +325,8 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
}
string musicVideoFolder = folderQueue.Dequeue();
Option<int> maybeParentFolder = await _libraryRepository.GetParentFolderId(musicVideoFolder);
// _logger.LogDebug("Scanning music video folder {Folder}", musicVideoFolder);
var allFiles = _localFileSystem.ListFiles(musicVideoFolder)
@ -323,19 +334,19 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -323,19 +334,19 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
.Filter(f => !Path.GetFileName(f).StartsWith("._", StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (string subdirectory in _localFileSystem.ListSubdirectories(musicVideoFolder)
.OrderBy(identity))
foreach (string subdirectory in _localFileSystem.ListSubdirectories(musicVideoFolder).OrderBy(identity))
{
folderQueue.Enqueue(subdirectory);
}
string etag = FolderEtag.Calculate(musicVideoFolder, _localFileSystem);
Option<LibraryFolder> knownFolder = libraryPath.LibraryFolders
.Filter(f => f.Path == musicVideoFolder)
.HeadOrNone();
LibraryFolder knownFolder = await _libraryRepository.GetOrAddFolder(
libraryPath,
maybeParentFolder,
musicVideoFolder);
// skip folder if etag matches
if (await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag)
if (knownFolder.Etag == etag)
{
continue;
}
@ -346,8 +357,9 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -346,8 +357,9 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
{
// TODO: figure out how to rebuild playouts
Either<BaseError, MediaItemScanResult<MusicVideo>> maybeMusicVideo = await _musicVideoRepository
.GetOrAdd(artist, libraryPath, file)
.GetOrAdd(artist, libraryPath, knownFolder, file)
.BindT(musicVideo => UpdateStatistics(musicVideo, ffmpegPath, ffprobePath))
.BindT(video => UpdateLibraryFolderId(video, knownFolder))
.BindT(UpdateMetadata)
.BindT(result => UpdateThumbnail(result, cancellationToken))
.BindT(UpdateSubtitles)
@ -385,6 +397,20 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan @@ -385,6 +397,20 @@ public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScan
return Unit.Default;
}
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> UpdateLibraryFolderId(
MediaItemScanResult<MusicVideo> video,
LibraryFolder libraryFolder)
{
MediaFile mediaFile = video.Item.GetHeadVersion().MediaFiles.Head();
if (mediaFile.LibraryFolderId != libraryFolder.Id)
{
await _libraryRepository.UpdateLibraryFolderId(mediaFile, libraryFolder.Id);
video.IsUpdated = true;
}
return video;
}
private async Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> UpdateMetadata(
MediaItemScanResult<MusicVideo> result)
{

39
ErsatzTV.Scanner/Core/Metadata/OtherVideoFolderScanner.cs

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
@ -77,6 +78,14 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -77,6 +78,14 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
var allFolders = new System.Collections.Generic.HashSet<string>();
var folderQueue = new Queue<string>();
string normalizedLibraryPath = libraryPath.Path.TrimEnd(
Path.DirectorySeparatorChar,
Path.AltDirectorySeparatorChar);
if (libraryPath.Path != normalizedLibraryPath)
{
await _libraryRepository.UpdatePath(libraryPath, normalizedLibraryPath);
}
if (ShouldIncludeFolder(libraryPath.Path) && allFolders.Add(libraryPath.Path))
{
folderQueue.Enqueue(libraryPath.Path);
@ -108,6 +117,8 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -108,6 +117,8 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
cancellationToken);
string otherVideoFolder = folderQueue.Dequeue();
Option<int> maybeParentFolder = await _libraryRepository.GetParentFolderId(otherVideoFolder);
foldersCompleted++;
var filesForEtag = _localFileSystem.ListFiles(otherVideoFolder).ToList();
@ -126,14 +137,13 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -126,14 +137,13 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
}
string etag = FolderEtag.Calculate(otherVideoFolder, _localFileSystem);
Option<LibraryFolder> knownFolder = libraryPath.LibraryFolders
.Filter(f => f.Path == otherVideoFolder)
.HeadOrNone();
LibraryFolder knownFolder = await _libraryRepository.GetOrAddFolder(
libraryPath,
maybeParentFolder,
otherVideoFolder);
// skip folder if etag matches
if (allFiles.Count == 0 ||
await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) ==
etag)
if (allFiles.Count == 0 || knownFolder.Etag == etag)
{
continue;
}
@ -147,8 +157,9 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -147,8 +157,9 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
foreach (string file in allFiles.OrderBy(identity))
{
Either<BaseError, MediaItemScanResult<OtherVideo>> maybeVideo = await _otherVideoRepository
.GetOrAdd(libraryPath, file)
.GetOrAdd(libraryPath, knownFolder, file)
.BindT(video => UpdateStatistics(video, ffmpegPath, ffprobePath))
.BindT(video => UpdateLibraryFolderId(video, knownFolder))
.BindT(UpdateMetadata)
.BindT(video => UpdateThumbnail(video, cancellationToken))
.BindT(UpdateSubtitles)
@ -223,6 +234,20 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan @@ -223,6 +234,20 @@ public class OtherVideoFolderScanner : LocalFolderScanner, IOtherVideoFolderScan
}
}
private async Task<Either<BaseError, MediaItemScanResult<OtherVideo>>> UpdateLibraryFolderId(
MediaItemScanResult<OtherVideo> video,
LibraryFolder libraryFolder)
{
MediaFile mediaFile = video.Item.GetHeadVersion().MediaFiles.Head();
if (mediaFile.LibraryFolderId != libraryFolder.Id)
{
await _libraryRepository.UpdateLibraryFolderId(mediaFile, libraryFolder.Id);
video.IsUpdated = true;
}
return video;
}
private async Task<Either<BaseError, MediaItemScanResult<OtherVideo>>> UpdateMetadata(
MediaItemScanResult<OtherVideo> result)
{

43
ErsatzTV.Scanner/Core/Metadata/SongFolderScanner.cs

@ -74,6 +74,14 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -74,6 +74,14 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
var folderQueue = new Queue<string>();
string normalizedLibraryPath = libraryPath.Path.TrimEnd(
Path.DirectorySeparatorChar,
Path.AltDirectorySeparatorChar);
if (libraryPath.Path != normalizedLibraryPath)
{
await _libraryRepository.UpdatePath(libraryPath, normalizedLibraryPath);
}
if (ShouldIncludeFolder(libraryPath.Path))
{
folderQueue.Enqueue(libraryPath.Path);
@ -104,6 +112,8 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -104,6 +112,8 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
cancellationToken);
string songFolder = folderQueue.Dequeue();
Option<int> maybeParentFolder = await _libraryRepository.GetParentFolderId(songFolder);
foldersCompleted++;
var filesForEtag = _localFileSystem.ListFiles(songFolder).ToList();
@ -121,14 +131,13 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -121,14 +131,13 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
}
string etag = FolderEtag.Calculate(songFolder, _localFileSystem);
Option<LibraryFolder> knownFolder = libraryPath.LibraryFolders
.Filter(f => f.Path == songFolder)
.HeadOrNone();
LibraryFolder knownFolder = await _libraryRepository.GetOrAddFolder(
libraryPath,
maybeParentFolder,
songFolder);
// skip folder if etag matches
if (allFiles.Count == 0 ||
await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) ==
etag)
if (allFiles.Count == 0 || knownFolder.Etag == etag)
{
continue;
}
@ -142,10 +151,11 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -142,10 +151,11 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
foreach (string file in allFiles.OrderBy(identity))
{
Either<BaseError, MediaItemScanResult<Song>> maybeSong = await _songRepository
.GetOrAdd(libraryPath, file)
.GetOrAdd(libraryPath, knownFolder, file)
.BindT(video => UpdateStatistics(video, ffmpegPath, ffprobePath))
.BindT(video => UpdateLibraryFolderId(video, knownFolder))
.BindT(UpdateMetadata)
.BindT(video => UpdateThumbnail(video, ffmpegPath, cancellationToken))
.BindT(video => UpdateThumbnail(video, knownFolder, ffmpegPath, cancellationToken))
.BindT(FlagNormal);
foreach (BaseError error in maybeSong.LeftToSeq())
@ -217,6 +227,20 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -217,6 +227,20 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
}
}
private async Task<Either<BaseError, MediaItemScanResult<Song>>> UpdateLibraryFolderId(
MediaItemScanResult<Song> result,
LibraryFolder libraryFolder)
{
MediaFile mediaFile = result.Item.GetHeadVersion().MediaFiles.Head();
if (mediaFile.LibraryFolderId != libraryFolder.Id)
{
await _libraryRepository.UpdateLibraryFolderId(mediaFile, libraryFolder.Id);
result.IsUpdated = true;
}
return result;
}
private async Task<Either<BaseError, MediaItemScanResult<Song>>> UpdateMetadata(MediaItemScanResult<Song> result)
{
try
@ -251,6 +275,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -251,6 +275,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
private async Task<Either<BaseError, MediaItemScanResult<Song>>> UpdateThumbnail(
MediaItemScanResult<Song> result,
LibraryFolder knownFolder,
string ffmpegPath,
CancellationToken cancellationToken)
{
@ -261,7 +286,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner @@ -261,7 +286,7 @@ public class SongFolderScanner : LocalFolderScanner, ISongFolderScanner
{
LibraryPath libraryPath = result.Item.LibraryPath;
string path = result.Item.GetHeadVersion().MediaFiles.Head().Path;
foreach (MediaItemScanResult<Song> s in (await _songRepository.GetOrAdd(libraryPath, path))
foreach (MediaItemScanResult<Song> s in (await _songRepository.GetOrAdd(libraryPath, knownFolder, path))
.RightToSeq())
{
result.Item = s.Item;

44
ErsatzTV.Scanner/Core/Metadata/TelevisionFolderScanner.cs

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
@ -77,6 +78,14 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -77,6 +78,14 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
{
decimal progressSpread = progressMax - progressMin;
string normalizedLibraryPath = libraryPath.Path.TrimEnd(
Path.DirectorySeparatorChar,
Path.AltDirectorySeparatorChar);
if (libraryPath.Path != normalizedLibraryPath)
{
await _libraryRepository.UpdatePath(libraryPath, normalizedLibraryPath);
}
var allShowFolders = _localFileSystem.ListSubdirectories(libraryPath.Path)
.Filter(ShouldIncludeFolder)
.OrderBy(identity)
@ -99,6 +108,14 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -99,6 +108,14 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
Array.Empty<int>()),
cancellationToken);
Option<int> maybeParentFolder = await _libraryRepository.GetParentFolderId(showFolder);
// this folder is unused by the show, but will be used as parents of season folders
LibraryFolder _ = await _libraryRepository.GetOrAddFolder(
libraryPath,
maybeParentFolder,
showFolder);
Either<BaseError, MediaItemScanResult<Show>> maybeShow =
await FindOrCreateShow(libraryPath.Id, showFolder)
.BindT(show => UpdateMetadataForShow(show, showFolder))
@ -220,13 +237,16 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -220,13 +237,16 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
return new ScanCanceled();
}
Option<int> maybeParentFolder = await _libraryRepository.GetParentFolderId(seasonFolder);
string etag = FolderEtag.CalculateWithSubfolders(seasonFolder, _localFileSystem);
Option<LibraryFolder> knownFolder = libraryPath.LibraryFolders
.Filter(f => f.Path == seasonFolder)
.HeadOrNone();
LibraryFolder knownFolder = await _libraryRepository.GetOrAddFolder(
libraryPath,
maybeParentFolder,
seasonFolder);
// skip folder if etag matches
if (await knownFolder.Map(f => f.Etag ?? string.Empty).IfNoneAsync(string.Empty) == etag)
if (knownFolder.Etag == etag)
{
continue;
}
@ -251,6 +271,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -251,6 +271,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
{
Either<BaseError, Unit> scanResult = await ScanEpisodes(
libraryPath,
knownFolder,
ffmpegPath,
ffprobePath,
season,
@ -283,6 +304,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -283,6 +304,7 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
private async Task<Either<BaseError, Unit>> ScanEpisodes(
LibraryPath libraryPath,
LibraryFolder seasonFolder,
string ffmpegPath,
string ffprobePath,
Season season,
@ -302,10 +324,11 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -302,10 +324,11 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
{
// TODO: figure out how to rebuild playlists
Either<BaseError, Episode> maybeEpisode = await _televisionRepository
.GetOrAddEpisode(season, libraryPath, file)
.GetOrAddEpisode(season, libraryPath, seasonFolder, file)
.BindT(
episode => UpdateStatistics(new MediaItemScanResult<Episode>(episode), ffmpegPath, ffprobePath)
.MapT(_ => episode))
.BindT(video => UpdateLibraryFolderId(video, seasonFolder))
.BindT(UpdateMetadata)
.BindT(e => UpdateThumbnail(e, cancellationToken))
.BindT(UpdateSubtitles)
@ -404,6 +427,17 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan @@ -404,6 +427,17 @@ public class TelevisionFolderScanner : LocalFolderScanner, ITelevisionFolderScan
return season;
}
private async Task<Either<BaseError, Episode>> UpdateLibraryFolderId(Episode episode, LibraryFolder libraryFolder)
{
MediaFile mediaFile = episode.GetHeadVersion().MediaFiles.Head();
if (mediaFile.LibraryFolderId != libraryFolder.Id)
{
await _libraryRepository.UpdateLibraryFolderId(mediaFile, libraryFolder.Id);
}
return episode;
}
private async Task<Either<BaseError, Episode>> UpdateMetadata(Episode episode)
{
try

Loading…
Cancel
Save