mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* initial remote stream library support; scanning seems to work ok * flood schedule remote streams kind of works * switch remote stream definitions to yaml files * implement remote stream script playback * update changelogpull/2176/head
86 changed files with 39340 additions and 101 deletions
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core.Search; |
||||
|
||||
namespace ErsatzTV.Application.MediaCards; |
||||
|
||||
public record RemoteStreamCardResultsViewModel(int Count, List<RemoteStreamCardViewModel> Cards, SearchPageMap PageMap); |
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Application.MediaCards; |
||||
|
||||
public record RemoteStreamCardViewModel( |
||||
int RemoteStreamId, |
||||
string Title, |
||||
string Subtitle, |
||||
string SortTitle, |
||||
string Poster, |
||||
MediaItemState State) : MediaCardViewModel( |
||||
RemoteStreamId, |
||||
Title, |
||||
Subtitle, |
||||
SortTitle, |
||||
Poster, |
||||
State) |
||||
{ |
||||
public int CustomIndex { get; set; } |
||||
} |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.MediaCollections; |
||||
|
||||
public record AddMediaItemToCollection(int CollectionId, int MediaItemId) : IRequest<Either<BaseError, Unit>>; |
@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
|
||||
using System.Threading.Channels; |
||||
using ErsatzTV.Application.Playouts; |
||||
using ErsatzTV.Application.Search; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.MediaCollections; |
||||
|
||||
public class AddMediaItemToCollectionHandler : |
||||
IRequestHandler<AddMediaItemToCollection, Either<BaseError, Unit>> |
||||
{ |
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository; |
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel; |
||||
|
||||
public AddMediaItemToCollectionHandler( |
||||
IDbContextFactory<TvContext> dbContextFactory, |
||||
IMediaCollectionRepository mediaCollectionRepository, |
||||
ChannelWriter<IBackgroundServiceRequest> channel, |
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> searchChannel) |
||||
{ |
||||
_dbContextFactory = dbContextFactory; |
||||
_mediaCollectionRepository = mediaCollectionRepository; |
||||
_channel = channel; |
||||
_searchChannel = searchChannel; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle( |
||||
AddMediaItemToCollection request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
Validation<BaseError, Parameters> validation = await Validate(dbContext, request); |
||||
return await validation.Apply(parameters => ApplyAddMediaItemRequest(dbContext, parameters)); |
||||
} |
||||
|
||||
private async Task<Unit> ApplyAddMediaItemRequest(TvContext dbContext, Parameters parameters) |
||||
{ |
||||
parameters.Collection.MediaItems.Add(parameters.MediaItem); |
||||
if (await dbContext.SaveChangesAsync() > 0) |
||||
{ |
||||
await _searchChannel.WriteAsync(new ReindexMediaItems([parameters.MediaItem.Id])); |
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository |
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id)) |
||||
{ |
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh)); |
||||
} |
||||
} |
||||
|
||||
return Unit.Default; |
||||
} |
||||
|
||||
private static async Task<Validation<BaseError, Parameters>> Validate( |
||||
TvContext dbContext, |
||||
AddMediaItemToCollection request) => |
||||
(await CollectionMustExist(dbContext, request), await ValidateMediaItem(dbContext, request)) |
||||
.Apply((collection, episode) => new Parameters(collection, episode)); |
||||
|
||||
private static Task<Validation<BaseError, Collection>> CollectionMustExist( |
||||
TvContext dbContext, |
||||
AddMediaItemToCollection request) => |
||||
dbContext.Collections |
||||
.Include(c => c.MediaItems) |
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId) |
||||
.Map(o => o.ToValidation<BaseError>("Collection does not exist.")); |
||||
|
||||
private static Task<Validation<BaseError, MediaItem>> ValidateMediaItem( |
||||
TvContext dbContext, |
||||
AddMediaItemToCollection request) => |
||||
dbContext.MediaItems |
||||
.SelectOneAsync(m => m.Id, e => e.Id == request.MediaItemId) |
||||
.Map(o => o.ToValidation<BaseError>("MediaItem does not exist")); |
||||
|
||||
private sealed record Parameters(Collection Collection, MediaItem MediaItem); |
||||
} |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.MediaItems; |
||||
|
||||
public record GetRemoteStreamById(int RemoteStreamId) : IRequest<Option<RemoteStreamViewModel>>; |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.MediaItems; |
||||
|
||||
public class GetRemoteStreamByIdHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<GetRemoteStreamById, Option<RemoteStreamViewModel>> |
||||
{ |
||||
public async Task<Option<RemoteStreamViewModel>> Handle( |
||||
GetRemoteStreamById request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
return await dbContext.RemoteStreams |
||||
.SelectOneAsync(rs => rs.Id, rs => rs.Id == request.RemoteStreamId) |
||||
.MapT(Mapper.ProjectToViewModel); |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.MediaItems; |
||||
|
||||
public record RemoteStreamViewModel(int Id, string Url, string Script); |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Application.MediaCards; |
||||
|
||||
namespace ErsatzTV.Application.Search; |
||||
|
||||
public record QuerySearchIndexRemoteStreams(string Query, int PageNumber, int PageSize) : IRequest<RemoteStreamCardResultsViewModel>; |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
using Bugsnag; |
||||
using ErsatzTV.Application.MediaCards; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Search; |
||||
using ErsatzTV.Core.Search; |
||||
using static ErsatzTV.Application.MediaCards.Mapper; |
||||
|
||||
namespace ErsatzTV.Application.Search; |
||||
|
||||
public class QuerySearchIndexRemoteStreamsHandler( |
||||
IClient client, |
||||
ISearchIndex searchIndex, |
||||
IRemoteStreamRepository imageRepository) |
||||
: IRequestHandler<QuerySearchIndexRemoteStreams, RemoteStreamCardResultsViewModel> |
||||
{ |
||||
public async Task<RemoteStreamCardResultsViewModel> Handle( |
||||
QuerySearchIndexRemoteStreams request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
SearchResult searchResult = await searchIndex.Search( |
||||
client, |
||||
request.Query, |
||||
string.Empty, |
||||
(request.PageNumber - 1) * request.PageSize, |
||||
request.PageSize); |
||||
|
||||
List<RemoteStreamCardViewModel> items = await imageRepository |
||||
.GetRemoteStreamsForCards(searchResult.Items.Map(i => i.Id).ToList()) |
||||
.Map(list => list.Map(ProjectToViewModel).ToList()); |
||||
|
||||
return new RemoteStreamCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); |
||||
} |
||||
} |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
using System.Diagnostics.CodeAnalysis; |
||||
|
||||
namespace ErsatzTV.Core.Domain; |
||||
|
||||
[SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix")] |
||||
public class RemoteStream : MediaItem |
||||
{ |
||||
public string Url { get; set; } |
||||
public string Script { get; set; } |
||||
public TimeSpan? Duration { get; set; } |
||||
public string FallbackQuery { get; set; } |
||||
public List<RemoteStreamMetadata> RemoteStreamMetadata { get; set; } |
||||
public List<MediaVersion> MediaVersions { get; set; } |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Core.Domain; |
||||
|
||||
public class RemoteStreamMetadata : Metadata |
||||
{ |
||||
public int RemoteStreamId { get; set; } |
||||
public RemoteStream RemoteStream { get; set; } |
||||
} |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Metadata; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories; |
||||
|
||||
public interface IRemoteStreamRepository |
||||
{ |
||||
Task<Either<BaseError, MediaItemScanResult<RemoteStream>>> GetOrAdd( |
||||
LibraryPath libraryPath, |
||||
LibraryFolder libraryFolder, |
||||
string path); |
||||
|
||||
Task<IEnumerable<string>> FindRemoteStreamPaths(LibraryPath libraryPath); |
||||
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path); |
||||
Task<bool> AddTag(RemoteStreamMetadata metadata, Tag tag); |
||||
Task<List<RemoteStreamMetadata>> GetRemoteStreamsForCards(List<int> ids); |
||||
Task UpdateDefinition(RemoteStream remoteStream); |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
using YamlDotNet.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.Streaming; |
||||
|
||||
public class YamlRemoteStreamDefinition |
||||
{ |
||||
public string Url { get; set; } |
||||
public string Script { get; set; } |
||||
public string Duration { get; set; } |
||||
[YamlMember(Alias = "fallback_query", ApplyNamingConventions = false)] |
||||
public string FallbackQuery { get; set; } |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,328 @@
@@ -0,0 +1,328 @@
|
||||
using System; |
||||
using Microsoft.EntityFrameworkCore.Metadata; |
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_RemoteStream : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Tag", |
||||
type: "int", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Subtitle", |
||||
type: "int", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Studio", |
||||
type: "int", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "MetadataGuid", |
||||
type: "int", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamId", |
||||
table: "MediaVersion", |
||||
type: "int", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Genre", |
||||
type: "int", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Artwork", |
||||
type: "int", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Actor", |
||||
type: "int", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "RemoteStream", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "int", nullable: false) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_RemoteStream", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_RemoteStream_MediaItem_Id", |
||||
column: x => x.Id, |
||||
principalTable: "MediaItem", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "RemoteStreamMetadata", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "int", nullable: false) |
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), |
||||
RemoteStreamId = table.Column<int>(type: "int", nullable: false), |
||||
MetadataKind = table.Column<int>(type: "int", nullable: false), |
||||
Title = table.Column<string>(type: "longtext", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
OriginalTitle = table.Column<string>(type: "longtext", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
SortTitle = table.Column<string>(type: "longtext", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
Year = table.Column<int>(type: "int", nullable: true), |
||||
ReleaseDate = table.Column<DateTime>(type: "datetime(6)", nullable: true), |
||||
DateAdded = table.Column<DateTime>(type: "datetime(6)", nullable: false), |
||||
DateUpdated = table.Column<DateTime>(type: "datetime(6)", nullable: false) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_RemoteStreamMetadata", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_RemoteStreamMetadata_RemoteStream_RemoteStreamId", |
||||
column: x => x.RemoteStreamId, |
||||
principalTable: "RemoteStream", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Tag_RemoteStreamMetadataId", |
||||
table: "Tag", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Subtitle_RemoteStreamMetadataId", |
||||
table: "Subtitle", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Studio_RemoteStreamMetadataId", |
||||
table: "Studio", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_MetadataGuid_RemoteStreamMetadataId", |
||||
table: "MetadataGuid", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_MediaVersion_RemoteStreamId", |
||||
table: "MediaVersion", |
||||
column: "RemoteStreamId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Genre_RemoteStreamMetadataId", |
||||
table: "Genre", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Artwork_RemoteStreamMetadataId", |
||||
table: "Artwork", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Actor_RemoteStreamMetadataId", |
||||
table: "Actor", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_RemoteStreamMetadata_RemoteStreamId", |
||||
table: "RemoteStreamMetadata", |
||||
column: "RemoteStreamId"); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Actor_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Actor", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Artwork_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Artwork", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Genre_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Genre", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_MediaVersion_RemoteStream_RemoteStreamId", |
||||
table: "MediaVersion", |
||||
column: "RemoteStreamId", |
||||
principalTable: "RemoteStream", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_MetadataGuid_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "MetadataGuid", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Studio_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Studio", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Subtitle_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Subtitle", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Tag_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Tag", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Actor_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Actor"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Artwork_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Artwork"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Genre_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Genre"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_MediaVersion_RemoteStream_RemoteStreamId", |
||||
table: "MediaVersion"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_MetadataGuid_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "MetadataGuid"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Studio_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Studio"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Subtitle_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Subtitle"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Tag_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Tag"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "RemoteStreamMetadata"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "RemoteStream"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Tag_RemoteStreamMetadataId", |
||||
table: "Tag"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Subtitle_RemoteStreamMetadataId", |
||||
table: "Subtitle"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Studio_RemoteStreamMetadataId", |
||||
table: "Studio"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_MetadataGuid_RemoteStreamMetadataId", |
||||
table: "MetadataGuid"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_MediaVersion_RemoteStreamId", |
||||
table: "MediaVersion"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Genre_RemoteStreamMetadataId", |
||||
table: "Genre"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Artwork_RemoteStreamMetadataId", |
||||
table: "Artwork"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Actor_RemoteStreamMetadataId", |
||||
table: "Actor"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Tag"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Subtitle"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Studio"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "MetadataGuid"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamId", |
||||
table: "MediaVersion"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Genre"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Artwork"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Actor"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_LocalLibraryRemoteStream : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
// create local images library
|
||||
migrationBuilder.Sql( |
||||
@"INSERT INTO Library (Name, MediaKind, MediaSourceId)
|
||||
SELECT 'Remote Streams', 7, Id FROM |
||||
(SELECT LMS.Id FROM LocalMediaSource LMS LIMIT 1) AS A");
|
||||
migrationBuilder.Sql("INSERT INTO LocalLibrary (Id) Values (last_insert_id())"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
|
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,62 @@
@@ -0,0 +1,62 @@
|
||||
using System; |
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_RemoteStreamFields : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<TimeSpan>( |
||||
name: "Duration", |
||||
table: "RemoteStream", |
||||
type: "time(6)", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "FallbackQuery", |
||||
table: "RemoteStream", |
||||
type: "longtext", |
||||
nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "Script", |
||||
table: "RemoteStream", |
||||
type: "longtext", |
||||
nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "Url", |
||||
table: "RemoteStream", |
||||
type: "longtext", |
||||
nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "Duration", |
||||
table: "RemoteStream"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "FallbackQuery", |
||||
table: "RemoteStream"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "Script", |
||||
table: "RemoteStream"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "Url", |
||||
table: "RemoteStream"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,323 @@
@@ -0,0 +1,323 @@
|
||||
using System; |
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_RemoteStream : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Tag", |
||||
type: "INTEGER", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Subtitle", |
||||
type: "INTEGER", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Studio", |
||||
type: "INTEGER", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "MetadataGuid", |
||||
type: "INTEGER", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamId", |
||||
table: "MediaVersion", |
||||
type: "INTEGER", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Genre", |
||||
type: "INTEGER", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Artwork", |
||||
type: "INTEGER", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Actor", |
||||
type: "INTEGER", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "RemoteStream", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_RemoteStream", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_RemoteStream_MediaItem_Id", |
||||
column: x => x.Id, |
||||
principalTable: "MediaItem", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "RemoteStreamMetadata", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true), |
||||
RemoteStreamId = table.Column<int>(type: "INTEGER", nullable: false), |
||||
MetadataKind = table.Column<int>(type: "INTEGER", nullable: false), |
||||
Title = table.Column<string>(type: "TEXT", nullable: true), |
||||
OriginalTitle = table.Column<string>(type: "TEXT", nullable: true), |
||||
SortTitle = table.Column<string>(type: "TEXT", nullable: true), |
||||
Year = table.Column<int>(type: "INTEGER", nullable: true), |
||||
ReleaseDate = table.Column<DateTime>(type: "TEXT", nullable: true), |
||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: false), |
||||
DateUpdated = table.Column<DateTime>(type: "TEXT", nullable: false) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_RemoteStreamMetadata", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_RemoteStreamMetadata_RemoteStream_RemoteStreamId", |
||||
column: x => x.RemoteStreamId, |
||||
principalTable: "RemoteStream", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Tag_RemoteStreamMetadataId", |
||||
table: "Tag", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Subtitle_RemoteStreamMetadataId", |
||||
table: "Subtitle", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Studio_RemoteStreamMetadataId", |
||||
table: "Studio", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_MetadataGuid_RemoteStreamMetadataId", |
||||
table: "MetadataGuid", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_MediaVersion_RemoteStreamId", |
||||
table: "MediaVersion", |
||||
column: "RemoteStreamId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Genre_RemoteStreamMetadataId", |
||||
table: "Genre", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Artwork_RemoteStreamMetadataId", |
||||
table: "Artwork", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Actor_RemoteStreamMetadataId", |
||||
table: "Actor", |
||||
column: "RemoteStreamMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_RemoteStreamMetadata_RemoteStreamId", |
||||
table: "RemoteStreamMetadata", |
||||
column: "RemoteStreamId"); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Actor_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Actor", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Artwork_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Artwork", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Genre_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Genre", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_MediaVersion_RemoteStream_RemoteStreamId", |
||||
table: "MediaVersion", |
||||
column: "RemoteStreamId", |
||||
principalTable: "RemoteStream", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_MetadataGuid_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "MetadataGuid", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Studio_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Studio", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Subtitle_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Subtitle", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Tag_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Tag", |
||||
column: "RemoteStreamMetadataId", |
||||
principalTable: "RemoteStreamMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Actor_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Actor"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Artwork_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Artwork"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Genre_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Genre"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_MediaVersion_RemoteStream_RemoteStreamId", |
||||
table: "MediaVersion"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_MetadataGuid_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "MetadataGuid"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Studio_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Studio"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Subtitle_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Subtitle"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Tag_RemoteStreamMetadata_RemoteStreamMetadataId", |
||||
table: "Tag"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "RemoteStreamMetadata"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "RemoteStream"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Tag_RemoteStreamMetadataId", |
||||
table: "Tag"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Subtitle_RemoteStreamMetadataId", |
||||
table: "Subtitle"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Studio_RemoteStreamMetadataId", |
||||
table: "Studio"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_MetadataGuid_RemoteStreamMetadataId", |
||||
table: "MetadataGuid"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_MediaVersion_RemoteStreamId", |
||||
table: "MediaVersion"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Genre_RemoteStreamMetadataId", |
||||
table: "Genre"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Artwork_RemoteStreamMetadataId", |
||||
table: "Artwork"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Actor_RemoteStreamMetadataId", |
||||
table: "Actor"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Tag"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Subtitle"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Studio"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "MetadataGuid"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamId", |
||||
table: "MediaVersion"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Genre"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Artwork"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "RemoteStreamMetadataId", |
||||
table: "Actor"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_LocalLibraryRemoteStream : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
// create local remote streams library
|
||||
migrationBuilder.Sql( |
||||
@"INSERT INTO Library (Name, MediaKind, MediaSourceId)
|
||||
SELECT 'Remote Streams', 7, Id FROM |
||||
(SELECT LMS.Id FROM LocalMediaSource LMS LIMIT 1)");
|
||||
migrationBuilder.Sql("INSERT INTO LocalLibrary (Id) Values (last_insert_rowid())"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
|
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
using System; |
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_RemoteStreamFields : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<TimeSpan>( |
||||
name: "Duration", |
||||
table: "RemoteStream", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "FallbackQuery", |
||||
table: "RemoteStream", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "Script", |
||||
table: "RemoteStream", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<string>( |
||||
name: "Url", |
||||
table: "RemoteStream", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "Duration", |
||||
table: "RemoteStream"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "FallbackQuery", |
||||
table: "RemoteStream"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "Script", |
||||
table: "RemoteStream"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "Url", |
||||
table: "RemoteStream"); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations; |
||||
|
||||
public class RemoteStreamConfiguration : IEntityTypeConfiguration<RemoteStream> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<RemoteStream> builder) |
||||
{ |
||||
builder.ToTable("RemoteStream"); |
||||
|
||||
builder.HasMany(i => i.RemoteStreamMetadata) |
||||
.WithOne(m => m.RemoteStream) |
||||
.HasForeignKey(m => m.RemoteStreamId) |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
|
||||
builder.HasMany(i => i.MediaVersions) |
||||
.WithOne() |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
} |
||||
} |
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations; |
||||
|
||||
public class RemoteStreamMetadataConfiguration : IEntityTypeConfiguration<RemoteStreamMetadata> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<RemoteStreamMetadata> builder) |
||||
{ |
||||
builder.ToTable("RemoteStreamMetadata"); |
||||
|
||||
builder.HasMany(sm => sm.Artwork) |
||||
.WithOne() |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
|
||||
builder.HasMany(sm => sm.Genres) |
||||
.WithOne() |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
|
||||
builder.HasMany(sm => sm.Tags) |
||||
.WithOne() |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
|
||||
builder.HasMany(sm => sm.Studios) |
||||
.WithOne() |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
|
||||
builder.HasMany(sm => sm.Actors) |
||||
.WithOne() |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
|
||||
builder.HasMany(mm => mm.Guids) |
||||
.WithOne() |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
|
||||
builder.HasMany(mm => mm.Subtitles) |
||||
.WithOne() |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
} |
||||
} |
@ -0,0 +1,172 @@
@@ -0,0 +1,172 @@
|
||||
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 RemoteStreamRepository( |
||||
IDbContextFactory<TvContext> dbContextFactory, |
||||
ILogger<RemoteStreamRepository> logger) |
||||
: IRemoteStreamRepository |
||||
{ |
||||
public async Task<Either<BaseError, MediaItemScanResult<RemoteStream>>> GetOrAdd( |
||||
LibraryPath libraryPath, |
||||
LibraryFolder libraryFolder, |
||||
string path) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); |
||||
Option<RemoteStream> maybeExisting = await dbContext.RemoteStreams |
||||
.AsNoTracking() |
||||
.Include(i => i.RemoteStreamMetadata) |
||||
.ThenInclude(ovm => ovm.Genres) |
||||
.Include(i => i.RemoteStreamMetadata) |
||||
.ThenInclude(ovm => ovm.Tags) |
||||
.Include(i => i.RemoteStreamMetadata) |
||||
.ThenInclude(ovm => ovm.Studios) |
||||
.Include(i => i.RemoteStreamMetadata) |
||||
.ThenInclude(ovm => ovm.Guids) |
||||
.Include(i => i.RemoteStreamMetadata) |
||||
.ThenInclude(ovm => ovm.Actors) |
||||
.Include(i => i.RemoteStreamMetadata) |
||||
.ThenInclude(ovm => ovm.Actors) |
||||
.ThenInclude(a => a.Artwork) |
||||
.Include(ov => ov.LibraryPath) |
||||
.ThenInclude(lp => lp.Library) |
||||
.Include(ov => ov.MediaVersions) |
||||
.ThenInclude(ov => ov.MediaFiles) |
||||
.Include(ov => ov.MediaVersions) |
||||
.ThenInclude(ov => ov.Streams) |
||||
.Include(ov => ov.TraktListItems) |
||||
.ThenInclude(tli => tli.TraktList) |
||||
.OrderBy(i => i.MediaVersions.First().MediaFiles.First().Path) |
||||
.SingleOrDefaultAsync(i => i.MediaVersions.First().MediaFiles.First().Path == path); |
||||
|
||||
return await maybeExisting.Match( |
||||
mediaItem => |
||||
Right<BaseError, MediaItemScanResult<RemoteStream>>( |
||||
new MediaItemScanResult<RemoteStream>(mediaItem) { IsAdded = false }).AsTask(), |
||||
async () => await AddRemoteStream(dbContext, libraryPath.Id, libraryFolder.Id, path)); |
||||
} |
||||
|
||||
public async Task<IEnumerable<string>> FindRemoteStreamPaths(LibraryPath libraryPath) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); |
||||
return await dbContext.Connection.QueryAsync<string>( |
||||
@"SELECT MF.Path
|
||||
FROM MediaFile MF |
||||
INNER JOIN MediaVersion MV on MF.MediaVersionId = MV.Id |
||||
INNER JOIN RemoteStream O on MV.RemoteStreamId = O.Id |
||||
INNER JOIN MediaItem MI on O.Id = MI.Id |
||||
WHERE MI.LibraryPathId = @LibraryPathId",
|
||||
new { LibraryPathId = libraryPath.Id }); |
||||
} |
||||
|
||||
public async Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); |
||||
|
||||
List<int> ids = await dbContext.Connection.QueryAsync<int>( |
||||
@"SELECT O.Id
|
||||
FROM RemoteStream O |
||||
INNER JOIN MediaItem MI on O.Id = MI.Id |
||||
INNER JOIN MediaVersion MV on O.Id = MV.RemoteStreamId |
||||
INNER JOIN MediaFile MF on MV.Id = MF.MediaVersionId |
||||
WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path",
|
||||
new { LibraryPathId = libraryPath.Id, Path = path }).Map(result => result.ToList()); |
||||
|
||||
foreach (int remoteStreamId in ids) |
||||
{ |
||||
RemoteStream remoteStream = await dbContext.RemoteStreams.FindAsync(remoteStreamId); |
||||
if (remoteStream != null) |
||||
{ |
||||
dbContext.RemoteStreams.Remove(remoteStream); |
||||
} |
||||
} |
||||
|
||||
await dbContext.SaveChangesAsync(); |
||||
|
||||
return ids; |
||||
} |
||||
|
||||
public async Task<bool> AddTag(RemoteStreamMetadata metadata, Tag tag) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); |
||||
return await dbContext.Connection.ExecuteAsync( |
||||
"INSERT INTO Tag (Name, RemoteStreamMetadataId, ExternalCollectionId) VALUES (@Name, @MetadataId, @ExternalCollectionId)", |
||||
new { tag.Name, MetadataId = metadata.Id, tag.ExternalCollectionId }).Map(result => result > 0); |
||||
} |
||||
|
||||
public async Task<List<RemoteStreamMetadata>> GetRemoteStreamsForCards(List<int> ids) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); |
||||
return await dbContext.RemoteStreamMetadata |
||||
.AsNoTracking() |
||||
.Filter(im => ids.Contains(im.RemoteStreamId)) |
||||
.Include(im => im.RemoteStream) |
||||
.Include(im => im.Artwork) |
||||
.Include(im => im.RemoteStream) |
||||
.ThenInclude(s => s.MediaVersions) |
||||
.ThenInclude(mv => mv.MediaFiles) |
||||
.OrderBy(im => im.SortTitle) |
||||
.ToListAsync(); |
||||
} |
||||
|
||||
public async Task UpdateDefinition(RemoteStream remoteStream) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(); |
||||
await dbContext.RemoteStreams |
||||
.Where(rs => rs.Id == remoteStream.Id) |
||||
.ExecuteUpdateAsync(setters => setters |
||||
.SetProperty(rs => rs.Url, remoteStream.Url) |
||||
.SetProperty(rs => rs.Script, remoteStream.Script) |
||||
.SetProperty(rs => rs.Duration, remoteStream.Duration) |
||||
.SetProperty(rs => rs.FallbackQuery, remoteStream.FallbackQuery)); |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<RemoteStream>>> AddRemoteStream( |
||||
TvContext dbContext, |
||||
int libraryPathId, |
||||
int libraryFolderId, |
||||
string path) |
||||
{ |
||||
try |
||||
{ |
||||
if (await MediaItemRepository.MediaFileAlreadyExists(path, libraryPathId, dbContext, logger)) |
||||
{ |
||||
return new MediaFileAlreadyExists(); |
||||
} |
||||
|
||||
var remoteStream = new RemoteStream |
||||
{ |
||||
LibraryPathId = libraryPathId, |
||||
MediaVersions = |
||||
[ |
||||
new MediaVersion |
||||
{ |
||||
MediaFiles = [new MediaFile { Path = path, LibraryFolderId = libraryFolderId }], |
||||
Streams = [] |
||||
} |
||||
], |
||||
TraktListItems = new List<TraktListItem> |
||||
{ |
||||
Capacity = 0 |
||||
} |
||||
}; |
||||
|
||||
await dbContext.RemoteStreams.AddAsync(remoteStream); |
||||
await dbContext.SaveChangesAsync(); |
||||
await dbContext.Entry(remoteStream).Reference(m => m.LibraryPath).LoadAsync(); |
||||
await dbContext.Entry(remoteStream.LibraryPath).Reference(lp => lp.Library).LoadAsync(); |
||||
return new MediaItemScanResult<RemoteStream>(remoteStream) { IsAdded = true }; |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
return BaseError.New(ex.Message); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||
|
||||
public interface IRemoteStreamFolderScanner |
||||
{ |
||||
Task<Either<BaseError, Unit>> ScanFolder( |
||||
LibraryPath libraryPath, |
||||
string ffmpegPath, |
||||
string ffprobePath, |
||||
decimal progressMin, |
||||
decimal progressMax, |
||||
CancellationToken cancellationToken); |
||||
} |
@ -0,0 +1,375 @@
@@ -0,0 +1,375 @@
|
||||
using System.Collections.Immutable; |
||||
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; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.MediaSources; |
||||
using ErsatzTV.Core.Metadata; |
||||
using ErsatzTV.Core.Streaming; |
||||
using ErsatzTV.Scanner.Core.Interfaces.FFmpeg; |
||||
using ErsatzTV.Scanner.Core.Interfaces.Metadata; |
||||
using Microsoft.Extensions.Logging; |
||||
using YamlDotNet.Serialization; |
||||
using YamlDotNet.Serialization.NamingConventions; |
||||
|
||||
namespace ErsatzTV.Scanner.Core.Metadata; |
||||
|
||||
public class RemoteStreamFolderScanner : LocalFolderScanner, IRemoteStreamFolderScanner |
||||
{ |
||||
private readonly IClient _client; |
||||
private readonly IRemoteStreamRepository _remoteStreamRepository; |
||||
private readonly ILibraryRepository _libraryRepository; |
||||
private readonly ILocalFileSystem _localFileSystem; |
||||
private readonly ILocalMetadataProvider _localMetadataProvider; |
||||
private readonly ILogger<RemoteStreamFolderScanner> _logger; |
||||
private readonly IMediaItemRepository _mediaItemRepository; |
||||
private readonly IMediator _mediator; |
||||
|
||||
public RemoteStreamFolderScanner( |
||||
ILocalFileSystem localFileSystem, |
||||
ILocalStatisticsProvider localStatisticsProvider, |
||||
ILocalMetadataProvider localMetadataProvider, |
||||
IMetadataRepository metadataRepository, |
||||
IImageCache imageCache, |
||||
IMediator mediator, |
||||
IRemoteStreamRepository remoteStreamRepository, |
||||
ILibraryRepository libraryRepository, |
||||
IMediaItemRepository mediaItemRepository, |
||||
IFFmpegPngService ffmpegPngService, |
||||
ITempFilePool tempFilePool, |
||||
IClient client, |
||||
ILogger<RemoteStreamFolderScanner> logger) : base( |
||||
localFileSystem, |
||||
localStatisticsProvider, |
||||
metadataRepository, |
||||
mediaItemRepository, |
||||
imageCache, |
||||
ffmpegPngService, |
||||
tempFilePool, |
||||
client, |
||||
logger) |
||||
{ |
||||
_localFileSystem = localFileSystem; |
||||
_localMetadataProvider = localMetadataProvider; |
||||
_mediator = mediator; |
||||
_remoteStreamRepository = remoteStreamRepository; |
||||
_libraryRepository = libraryRepository; |
||||
_mediaItemRepository = mediaItemRepository; |
||||
_client = client; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, Unit>> ScanFolder( |
||||
LibraryPath libraryPath, |
||||
string ffmpegPath, |
||||
string ffprobePath, |
||||
decimal progressMin, |
||||
decimal progressMax, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
try |
||||
{ |
||||
IDeserializer deserializer = new DeserializerBuilder() |
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance) |
||||
.Build(); |
||||
|
||||
decimal progressSpread = progressMax - progressMin; |
||||
|
||||
var foldersCompleted = 0; |
||||
|
||||
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); |
||||
} |
||||
|
||||
ImmutableHashSet<string> allTrashedItems = await _mediaItemRepository.GetAllTrashedItems(libraryPath); |
||||
|
||||
if (ShouldIncludeFolder(libraryPath.Path) && allFolders.Add(libraryPath.Path)) |
||||
{ |
||||
folderQueue.Enqueue(libraryPath.Path); |
||||
} |
||||
|
||||
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path) |
||||
.Filter(ShouldIncludeFolder) |
||||
.Filter(allFolders.Add) |
||||
.OrderBy(identity)) |
||||
{ |
||||
folderQueue.Enqueue(folder); |
||||
} |
||||
|
||||
while (folderQueue.Count > 0) |
||||
{ |
||||
if (cancellationToken.IsCancellationRequested) |
||||
{ |
||||
return new ScanCanceled(); |
||||
} |
||||
|
||||
decimal percentCompletion = (decimal)foldersCompleted / (foldersCompleted + folderQueue.Count); |
||||
await _mediator.Publish( |
||||
new ScannerProgressUpdate( |
||||
libraryPath.LibraryId, |
||||
null, |
||||
progressMin + percentCompletion * progressSpread, |
||||
[], |
||||
[]), |
||||
cancellationToken); |
||||
|
||||
string remoteStreamFolder = folderQueue.Dequeue(); |
||||
Option<int> maybeParentFolder = await _libraryRepository.GetParentFolderId(libraryPath, remoteStreamFolder); |
||||
|
||||
foldersCompleted++; |
||||
|
||||
var filesForEtag = _localFileSystem.ListFiles(remoteStreamFolder).ToList(); |
||||
|
||||
var allFiles = filesForEtag |
||||
.Filter(f => RemoteStreamExtensions.Contains(Path.GetExtension(f).Replace(".", string.Empty))) |
||||
.Filter(f => !Path.GetFileName(f).StartsWith("._", StringComparison.OrdinalIgnoreCase)) |
||||
.ToList(); |
||||
|
||||
foreach (string subdirectory in _localFileSystem.ListSubdirectories(remoteStreamFolder) |
||||
.Filter(ShouldIncludeFolder) |
||||
.Filter(allFolders.Add) |
||||
.OrderBy(identity)) |
||||
{ |
||||
folderQueue.Enqueue(subdirectory); |
||||
} |
||||
|
||||
string etag = FolderEtag.Calculate(remoteStreamFolder, _localFileSystem); |
||||
LibraryFolder knownFolder = await _libraryRepository.GetOrAddFolder( |
||||
libraryPath, |
||||
maybeParentFolder, |
||||
remoteStreamFolder); |
||||
|
||||
if (knownFolder.Etag == etag) |
||||
{ |
||||
if (allFiles.Any(allTrashedItems.Contains)) |
||||
{ |
||||
_logger.LogDebug("Previously trashed items are now present in folder {Folder}", remoteStreamFolder); |
||||
} |
||||
else |
||||
{ |
||||
// etag matches and no trashed items are now present, continue to next folder
|
||||
continue; |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
_logger.LogDebug( |
||||
"UPDATE: Etag has changed for folder {Folder}", |
||||
remoteStreamFolder); |
||||
} |
||||
|
||||
var hasErrors = false; |
||||
|
||||
foreach (string file in allFiles.OrderBy(identity)) |
||||
{ |
||||
Either<BaseError, MediaItemScanResult<RemoteStream>> maybeVideo = await _remoteStreamRepository |
||||
.GetOrAdd(libraryPath, knownFolder, file) |
||||
.BindT(video => ParseRemoteStreamDefinition(video, deserializer, cancellationToken)) |
||||
.BindT(video => UpdateStatistics(video, ffmpegPath, ffprobePath)) |
||||
.BindT(video => UpdateLibraryFolderId(video, knownFolder)) |
||||
.BindT(UpdateMetadata) |
||||
//.BindT(video => UpdateThumbnail(video, cancellationToken))
|
||||
//.BindT(UpdateSubtitles)
|
||||
.BindT(FlagNormal); |
||||
|
||||
foreach (BaseError error in maybeVideo.LeftToSeq()) |
||||
{ |
||||
_logger.LogWarning("Error processing remote stream at {Path}: {Error}", file, error.Value); |
||||
hasErrors = true; |
||||
} |
||||
|
||||
foreach (MediaItemScanResult<RemoteStream> result in maybeVideo.RightToSeq()) |
||||
{ |
||||
if (result.IsAdded || result.IsUpdated) |
||||
{ |
||||
await _mediator.Publish( |
||||
new ScannerProgressUpdate( |
||||
libraryPath.LibraryId, |
||||
null, |
||||
null, |
||||
[result.Item.Id], |
||||
[]), |
||||
cancellationToken); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// only do this once per folder and only if all files processed successfully
|
||||
if (!hasErrors) |
||||
{ |
||||
await _libraryRepository.SetEtag(libraryPath, knownFolder, remoteStreamFolder, etag); |
||||
} |
||||
} |
||||
|
||||
foreach (string path in await _remoteStreamRepository.FindRemoteStreamPaths(libraryPath)) |
||||
{ |
||||
if (!_localFileSystem.FileExists(path)) |
||||
{ |
||||
_logger.LogInformation("Flagging missing remote stream at {Path}", path); |
||||
List<int> remoteStreamIds = await FlagFileNotFound(libraryPath, path); |
||||
await _mediator.Publish( |
||||
new ScannerProgressUpdate( |
||||
libraryPath.LibraryId, |
||||
null, |
||||
null, |
||||
remoteStreamIds.ToArray(), |
||||
[]), |
||||
cancellationToken); |
||||
} |
||||
else if (Path.GetFileName(path).StartsWith("._", StringComparison.OrdinalIgnoreCase)) |
||||
{ |
||||
_logger.LogInformation("Removing dot underscore file at {Path}", path); |
||||
List<int> remoteStreamIds = await _remoteStreamRepository.DeleteByPath(libraryPath, path); |
||||
await _mediator.Publish( |
||||
new ScannerProgressUpdate( |
||||
libraryPath.LibraryId, |
||||
null, |
||||
null, |
||||
[], |
||||
remoteStreamIds.ToArray()), |
||||
cancellationToken); |
||||
} |
||||
} |
||||
|
||||
await _libraryRepository.CleanEtagsForLibraryPath(libraryPath); |
||||
|
||||
return Unit.Default; |
||||
} |
||||
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) |
||||
{ |
||||
return new ScanCanceled(); |
||||
} |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<RemoteStream>>> UpdateLibraryFolderId( |
||||
MediaItemScanResult<RemoteStream> 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<RemoteStream>>> ParseRemoteStreamDefinition( |
||||
MediaItemScanResult<RemoteStream> result, |
||||
IDeserializer deserializer, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
try |
||||
{ |
||||
RemoteStream remoteStream = result.Item; |
||||
|
||||
string path = remoteStream.GetHeadVersion().MediaFiles.Head().Path; |
||||
string yaml = await File.ReadAllTextAsync(path, cancellationToken); |
||||
YamlRemoteStreamDefinition definition = deserializer.Deserialize<YamlRemoteStreamDefinition>(yaml); |
||||
|
||||
bool updated = false; |
||||
if (remoteStream.Url != definition.Url) |
||||
{ |
||||
remoteStream.Url = definition.Url; |
||||
updated = true; |
||||
} |
||||
|
||||
if (remoteStream.Script != definition.Script) |
||||
{ |
||||
remoteStream.Script = definition.Script; |
||||
updated = true; |
||||
} |
||||
|
||||
if (TimeSpan.TryParse(definition.Duration, out TimeSpan duration)) |
||||
{ |
||||
if (remoteStream.Duration != duration) |
||||
{ |
||||
remoteStream.Duration = duration; |
||||
updated = true; |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
if (remoteStream.Duration is not null) |
||||
{ |
||||
remoteStream.Duration = null; |
||||
updated = true; |
||||
} |
||||
} |
||||
|
||||
if (remoteStream.FallbackQuery != definition.FallbackQuery) |
||||
{ |
||||
remoteStream.FallbackQuery = definition.FallbackQuery; |
||||
updated = true; |
||||
} |
||||
|
||||
if (string.IsNullOrEmpty(remoteStream.Url) && string.IsNullOrEmpty(remoteStream.Script)) |
||||
{ |
||||
return BaseError.New($"`url` or `script` is required in remote stream definition file {path}"); |
||||
} |
||||
|
||||
if (updated) |
||||
{ |
||||
await _remoteStreamRepository.UpdateDefinition(remoteStream); |
||||
result.IsUpdated = true; |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_client.Notify(ex); |
||||
return BaseError.New(ex.ToString()); |
||||
} |
||||
} |
||||
|
||||
private async Task<Either<BaseError, MediaItemScanResult<RemoteStream>>> UpdateMetadata( |
||||
MediaItemScanResult<RemoteStream> result) |
||||
{ |
||||
try |
||||
{ |
||||
RemoteStream remoteStream = result.Item; |
||||
string path = remoteStream.GetHeadVersion().MediaFiles.Head().Path; |
||||
var shouldUpdate = true; |
||||
|
||||
foreach (RemoteStreamMetadata remoteStreamMetadata in Optional(remoteStream.RemoteStreamMetadata) |
||||
.Flatten() |
||||
.HeadOrNone()) |
||||
{ |
||||
shouldUpdate = remoteStreamMetadata.MetadataKind == MetadataKind.Fallback || |
||||
remoteStreamMetadata.DateUpdated != _localFileSystem.GetLastWriteTime(path); |
||||
} |
||||
|
||||
if (shouldUpdate) |
||||
{ |
||||
remoteStream.RemoteStreamMetadata ??= []; |
||||
|
||||
_logger.LogDebug("Refreshing {Attribute} for {Path}", "Metadata", path); |
||||
if (await _localMetadataProvider.RefreshTagMetadata(remoteStream)) |
||||
{ |
||||
result.IsUpdated = true; |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_client.Notify(ex); |
||||
return BaseError.New(ex.ToString()); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,132 @@
@@ -0,0 +1,132 @@
|
||||
@page "/media/remote/streams" |
||||
@page "/media/remote/streams/page/{PageNumber:int}" |
||||
@using ErsatzTV.Application.MediaCards |
||||
@using ErsatzTV.Application.MediaCollections |
||||
@using ErsatzTV.Application.Search |
||||
@using ErsatzTV.Extensions |
||||
@inherits MultiSelectBase<RemoteStreamList> |
||||
@inject NavigationManager NavigationManager |
||||
|
||||
<MudForm Style="max-height: 100%"> |
||||
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100;"> |
||||
<MediaCardPager Query="@_query" |
||||
PageNumber="@PageNumber" |
||||
PageSize="@PageSize" |
||||
TotalCount="@_data.Count" |
||||
NextPage="@NextPage" |
||||
PrevPage="@PrevPage" |
||||
AddSelectionToCollection="@AddSelectionToCollection" |
||||
AddSelectionToPlaylist="@AddSelectionToPlaylist" |
||||
ClearSelection="@ClearSelection" |
||||
IsSelectMode="@IsSelectMode" |
||||
SelectionLabel="@SelectionLabel"/> |
||||
</MudPaper> |
||||
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto"> |
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> |
||||
<MudStack Row="true" Wrap="Wrap.Wrap"> |
||||
<FragmentLetterAnchor TCard="RemoteStreamCardViewModel" Cards="@_data.Cards"> |
||||
<MediaCard Data="@context" |
||||
Href="" |
||||
ArtworkKind="ArtworkKind.Thumbnail" |
||||
AddToCollectionClicked="@AddToCollection" |
||||
SelectClicked="@(e => SelectClicked(context, e))" |
||||
IsSelected="@IsSelected(context)" |
||||
IsSelectMode="@IsSelectMode()"/> |
||||
</FragmentLetterAnchor> |
||||
</MudStack> |
||||
</MudContainer> |
||||
</div> |
||||
</MudForm> |
||||
@if (_data.PageMap is not null) |
||||
{ |
||||
<LetterBar PageMap="@_data.PageMap" |
||||
BaseUri="media/remote/streams" |
||||
Query="@_query"/> |
||||
} |
||||
|
||||
@code { |
||||
private static int PageSize => 100; |
||||
|
||||
[Parameter] |
||||
public int PageNumber { get; set; } |
||||
|
||||
private RemoteStreamCardResultsViewModel _data = new(0, new List<RemoteStreamCardViewModel>(), null); |
||||
private string _query; |
||||
|
||||
protected override async Task OnParametersSetAsync() |
||||
{ |
||||
if (PageNumber == 0) |
||||
{ |
||||
PageNumber = 1; |
||||
} |
||||
|
||||
_query = NavigationManager.Uri.GetSearchQuery(); |
||||
|
||||
await RefreshData(); |
||||
} |
||||
|
||||
protected override async Task RefreshData() |
||||
{ |
||||
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:remote_stream" : $"type:remote_stream AND ({_query})"; |
||||
_data = await Mediator.Send(new QuerySearchIndexRemoteStreams(searchQuery, PageNumber, PageSize), CancellationToken); |
||||
} |
||||
|
||||
private void PrevPage() |
||||
{ |
||||
var uri = $"media/remote/streams/page/{PageNumber - 1}"; |
||||
if (!string.IsNullOrWhiteSpace(_query)) |
||||
{ |
||||
(string key, string value) = _query.EncodeQuery(); |
||||
uri = $"{uri}?{key}={value}"; |
||||
} |
||||
|
||||
NavigationManager.NavigateTo(uri); |
||||
} |
||||
|
||||
private void NextPage() |
||||
{ |
||||
var uri = $"media/remote/streams/page/{PageNumber + 1}"; |
||||
if (!string.IsNullOrWhiteSpace(_query)) |
||||
{ |
||||
(string key, string value) = _query.EncodeQuery(); |
||||
uri = $"{uri}?{key}={value}"; |
||||
} |
||||
|
||||
NavigationManager.NavigateTo(uri); |
||||
} |
||||
|
||||
private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) |
||||
{ |
||||
List<MediaCardViewModel> GetSortedItems() |
||||
{ |
||||
return _data.Cards.OrderBy(m => m.SortTitle).ToList<MediaCardViewModel>(); |
||||
} |
||||
|
||||
SelectClicked(GetSortedItems, card, e); |
||||
} |
||||
|
||||
private async Task AddToCollection(MediaCardViewModel card) |
||||
{ |
||||
if (card is RemoteStreamCardViewModel remoteStream) |
||||
{ |
||||
var parameters = new DialogParameters { { "EntityType", "remote stream" }, { "EntityName", remoteStream.Title } }; |
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; |
||||
|
||||
IDialogReference dialog = await Dialog.ShowAsync<AddToCollectionDialog>("Add To Collection", parameters, options); |
||||
DialogResult result = await dialog.Result; |
||||
if (result is { Canceled: false, Data: MediaCollectionViewModel collection }) |
||||
{ |
||||
var request = new AddMediaItemToCollection(collection.Id, remoteStream.RemoteStreamId); |
||||
Either<BaseError, Unit> addResult = await Mediator.Send(request, CancellationToken); |
||||
addResult.Match( |
||||
Left: error => |
||||
{ |
||||
Snackbar.Add($"Unexpected error adding remote stream to collection: {error.Value}"); |
||||
Logger.LogError("Unexpected error adding remote stream to collection: {Error}", error.Value); |
||||
}, |
||||
Right: _ => Snackbar.Add($"Added {remoteStream.Title} to collection {collection.Name}", Severity.Success)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue