mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* sync guids/provider ids (#227) * sync guids from plex * cleanup * sync local guids * sync jellyfin and emby guids * add episodes to search index (#228) * sync episode directors and writers * display episode writers and directors * remove missing episodes from search index * show episodes in search results * fix emby and jellyfin episode updates * fix updating plex episodes * don't delete channel logos on startup * add episodes page; fix adding episodes to collection * cleanup * multi-part episode grouping fixes (#229)pull/231/head
78 changed files with 8323 additions and 479 deletions
@ -1,6 +1,11 @@
@@ -1,6 +1,11 @@
|
||||
using System.Collections.Generic; |
||||
using ErsatzTV.Core.Search; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.Application.MediaCards |
||||
{ |
||||
public record TelevisionEpisodeCardResultsViewModel(int Count, List<TelevisionEpisodeCardViewModel> Cards); |
||||
public record TelevisionEpisodeCardResultsViewModel( |
||||
int Count, |
||||
List<TelevisionEpisodeCardViewModel> Cards, |
||||
Option<SearchPageMap> PageMap); |
||||
} |
||||
|
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Application.MediaCards; |
||||
using MediatR; |
||||
|
||||
namespace ErsatzTV.Application.Search.Queries |
||||
{ |
||||
public record QuerySearchIndexEpisodes |
||||
(string Query, int PageNumber, int PageSize) : IRequest<TelevisionEpisodeCardResultsViewModel>; |
||||
} |
||||
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Application.MediaCards; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Search; |
||||
using ErsatzTV.Core.Search; |
||||
using LanguageExt; |
||||
using MediatR; |
||||
using static ErsatzTV.Application.MediaCards.Mapper; |
||||
|
||||
namespace ErsatzTV.Application.Search.Queries |
||||
{ |
||||
public class |
||||
QuerySearchIndexEpisodesHandler : IRequestHandler<QuerySearchIndexEpisodes, |
||||
TelevisionEpisodeCardResultsViewModel> |
||||
{ |
||||
private readonly IMediaSourceRepository _mediaSourceRepository; |
||||
private readonly ISearchIndex _searchIndex; |
||||
private readonly ITelevisionRepository _televisionRepository; |
||||
|
||||
public QuerySearchIndexEpisodesHandler( |
||||
ISearchIndex searchIndex, |
||||
ITelevisionRepository televisionRepository, |
||||
IMediaSourceRepository mediaSourceRepository) |
||||
{ |
||||
_searchIndex = searchIndex; |
||||
_televisionRepository = televisionRepository; |
||||
_mediaSourceRepository = mediaSourceRepository; |
||||
} |
||||
|
||||
public async Task<TelevisionEpisodeCardResultsViewModel> Handle( |
||||
QuerySearchIndexEpisodes request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
SearchResult searchResult = await _searchIndex.Search( |
||||
request.Query, |
||||
(request.PageNumber - 1) * request.PageSize, |
||||
request.PageSize); |
||||
|
||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin() |
||||
.Map(list => list.HeadOrNone()); |
||||
|
||||
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby() |
||||
.Map(list => list.HeadOrNone()); |
||||
|
||||
List<TelevisionEpisodeCardViewModel> items = await _televisionRepository |
||||
.GetEpisodesForCards(searchResult.Items.Map(i => i.Id).ToList()) |
||||
.Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby, true)).ToList()); |
||||
|
||||
return new TelevisionEpisodeCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain |
||||
{ |
||||
public class MetadataGuid |
||||
{ |
||||
public int Id { get; set; } |
||||
public string Guid { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.Metadata.Nfo |
||||
{ |
||||
public class UniqueIdNfo |
||||
{ |
||||
[XmlAttribute("default")] |
||||
public bool Default { get; set; } |
||||
|
||||
[XmlAttribute("type")] |
||||
public string Type { get; set; } |
||||
|
||||
[XmlText] |
||||
public string Guid { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations |
||||
{ |
||||
public class MetadataGuidConfiguration : IEntityTypeConfiguration<MetadataGuid> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<MetadataGuid> builder) => builder.ToTable("MetadataGuid"); |
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Infrastructure.Emby.Models |
||||
{ |
||||
public class EmbyProviderIdsResponse |
||||
{ |
||||
public string Imdb { get; set; } |
||||
public string Tmdb { get; set; } |
||||
public string Tvdb { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Infrastructure.Jellyfin.Models |
||||
{ |
||||
public class JellyfinProviderIdsResponse |
||||
{ |
||||
public string Imdb { get; set; } |
||||
public string Tmdb { get; set; } |
||||
public string Tvdb { get; set; } |
||||
} |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,124 @@
@@ -0,0 +1,124 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations |
||||
{ |
||||
public partial class Add_MetadataGuid : Migration |
||||
{ |
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
// local and plex
|
||||
migrationBuilder.Sql("UPDATE MovieMetadata SET DateUpdated = '0001-01-01 00:00:00'"); |
||||
migrationBuilder.Sql("UPDATE ShowMetadata SET DateUpdated = '0001-01-01 00:00:00'"); |
||||
migrationBuilder.Sql("UPDATE SeasonMetadata SET DateUpdated = '0001-01-01 00:00:00'"); |
||||
migrationBuilder.Sql("UPDATE EpisodeMetadata SET DateUpdated = '0001-01-01 00:00:00'"); |
||||
migrationBuilder.Sql( |
||||
@"UPDATE LibraryFolder SET Etag = NULL WHERE LibraryPathId IN
|
||||
(SELECT LibraryPathId FROM LibraryPath LP |
||||
INNER JOIN Library L on LP.LibraryId = L.Id |
||||
WHERE L.MediaKind = 1)");
|
||||
|
||||
// emby
|
||||
migrationBuilder.Sql("UPDATE EmbyMovie SET Etag = NULL"); |
||||
migrationBuilder.Sql("UPDATE EmbyShow SET Etag = NULL"); |
||||
migrationBuilder.Sql("UPDATE EmbySeason SET Etag = NULL"); |
||||
migrationBuilder.Sql("UPDATE EmbyEpisode SET Etag = NULL"); |
||||
|
||||
// jellyfin
|
||||
migrationBuilder.Sql("UPDATE JellyfinMovie SET Etag = NULL"); |
||||
migrationBuilder.Sql("UPDATE JellyfinShow SET Etag = NULL"); |
||||
migrationBuilder.Sql("UPDATE JellyfinSeason SET Etag = NULL"); |
||||
migrationBuilder.Sql("UPDATE JellyfinEpisode SET Etag = NULL"); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "MetadataGuid", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true), |
||||
Guid = table.Column<string>(type: "TEXT", nullable: true), |
||||
ArtistMetadataId = table.Column<int>(type: "INTEGER", nullable: true), |
||||
EpisodeMetadataId = table.Column<int>(type: "INTEGER", nullable: true), |
||||
MovieMetadataId = table.Column<int>(type: "INTEGER", nullable: true), |
||||
MusicVideoMetadataId = table.Column<int>(type: "INTEGER", nullable: true), |
||||
SeasonMetadataId = table.Column<int>(type: "INTEGER", nullable: true), |
||||
ShowMetadataId = table.Column<int>(type: "INTEGER", nullable: true) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_MetadataGuid", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_MetadataGuid_ArtistMetadata_ArtistMetadataId", |
||||
column: x => x.ArtistMetadataId, |
||||
principalTable: "ArtistMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Restrict); |
||||
table.ForeignKey( |
||||
name: "FK_MetadataGuid_EpisodeMetadata_EpisodeMetadataId", |
||||
column: x => x.EpisodeMetadataId, |
||||
principalTable: "EpisodeMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_MetadataGuid_MovieMetadata_MovieMetadataId", |
||||
column: x => x.MovieMetadataId, |
||||
principalTable: "MovieMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_MetadataGuid_MusicVideoMetadata_MusicVideoMetadataId", |
||||
column: x => x.MusicVideoMetadataId, |
||||
principalTable: "MusicVideoMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Restrict); |
||||
table.ForeignKey( |
||||
name: "FK_MetadataGuid_SeasonMetadata_SeasonMetadataId", |
||||
column: x => x.SeasonMetadataId, |
||||
principalTable: "SeasonMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_MetadataGuid_ShowMetadata_ShowMetadataId", |
||||
column: x => x.ShowMetadataId, |
||||
principalTable: "ShowMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_MetadataGuid_ArtistMetadataId", |
||||
table: "MetadataGuid", |
||||
column: "ArtistMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_MetadataGuid_EpisodeMetadataId", |
||||
table: "MetadataGuid", |
||||
column: "EpisodeMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_MetadataGuid_MovieMetadataId", |
||||
table: "MetadataGuid", |
||||
column: "MovieMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_MetadataGuid_MusicVideoMetadataId", |
||||
table: "MetadataGuid", |
||||
column: "MusicVideoMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_MetadataGuid_SeasonMetadataId", |
||||
table: "MetadataGuid", |
||||
column: "SeasonMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_MetadataGuid_ShowMetadataId", |
||||
table: "MetadataGuid", |
||||
column: "ShowMetadataId"); |
||||
} |
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropTable( |
||||
name: "MetadataGuid"); |
||||
} |
||||
} |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Migrations |
||||
{ |
||||
public partial class Add_EpisodeMetadataDirectorsWriters : Migration |
||||
{ |
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<int>( |
||||
name: "EpisodeMetadataId", |
||||
table: "Writer", |
||||
type: "INTEGER", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "EpisodeMetadataId", |
||||
table: "Director", |
||||
type: "INTEGER", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Writer_EpisodeMetadataId", |
||||
table: "Writer", |
||||
column: "EpisodeMetadataId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Director_EpisodeMetadataId", |
||||
table: "Director", |
||||
column: "EpisodeMetadataId"); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Director_EpisodeMetadata_EpisodeMetadataId", |
||||
table: "Director", |
||||
column: "EpisodeMetadataId", |
||||
principalTable: "EpisodeMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Writer_EpisodeMetadata_EpisodeMetadataId", |
||||
table: "Writer", |
||||
column: "EpisodeMetadataId", |
||||
principalTable: "EpisodeMetadata", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
} |
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Director_EpisodeMetadata_EpisodeMetadataId", |
||||
table: "Director"); |
||||
|
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Writer_EpisodeMetadata_EpisodeMetadataId", |
||||
table: "Writer"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Writer_EpisodeMetadataId", |
||||
table: "Writer"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Director_EpisodeMetadataId", |
||||
table: "Director"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "EpisodeMetadataId", |
||||
table: "Writer"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "EpisodeMetadataId", |
||||
table: "Director"); |
||||
} |
||||
} |
||||
} |
||||
@ -1,7 +1,10 @@
@@ -1,7 +1,10 @@
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
{ |
||||
public class PlexDirectorResponse |
||||
{ |
||||
[XmlAttribute("tag")] |
||||
public string Tag { get; set; } |
||||
} |
||||
} |
||||
|
||||
@ -1,9 +1,16 @@
@@ -1,9 +1,16 @@
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
{ |
||||
public class PlexGenreResponse |
||||
{ |
||||
[XmlAttribute("id")] |
||||
public int Id { get; set; } |
||||
|
||||
[XmlAttribute("filter")] |
||||
public string Filter { get; set; } |
||||
|
||||
[XmlAttribute("tag")] |
||||
public string Tag { get; set; } |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
{ |
||||
public class PlexGuidResponse |
||||
{ |
||||
[XmlAttribute("id")] |
||||
public string Id { get; set; } |
||||
} |
||||
} |
||||
@ -1,21 +1,50 @@
@@ -1,21 +1,50 @@
|
||||
using System.Collections.Generic; |
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
{ |
||||
public class PlexMediaResponse |
||||
public class PlexMediaResponse<T> |
||||
{ |
||||
[XmlAttribute("id")] |
||||
public int Id { get; set; } |
||||
|
||||
[XmlAttribute("duration")] |
||||
public int Duration { get; set; } |
||||
|
||||
[XmlAttribute("bitrate")] |
||||
public int Bitrate { get; set; } |
||||
|
||||
[XmlAttribute("width")] |
||||
public int Width { get; set; } |
||||
|
||||
[XmlAttribute("height")] |
||||
public int Height { get; set; } |
||||
|
||||
[XmlAttribute("aspectRatio")] |
||||
public double AspectRatio { get; set; } |
||||
|
||||
[XmlAttribute("audioChannels")] |
||||
public int AudioChannels { get; set; } |
||||
|
||||
[XmlAttribute("audioCodec")] |
||||
public string AudioCodec { get; set; } |
||||
|
||||
[XmlAttribute("videoCodec")] |
||||
public string VideoCodec { get; set; } |
||||
|
||||
[XmlAttribute("videoResulution")] |
||||
public string VideoResolution { get; set; } |
||||
|
||||
[XmlAttribute("videoProfile")] |
||||
public string VideoProfile { get; set; } |
||||
|
||||
[XmlAttribute("container")] |
||||
public string Container { get; set; } |
||||
|
||||
[XmlAttribute("videoFrameRate")] |
||||
public string VideoFrameRate { get; set; } |
||||
public List<PlexPartResponse> Part { get; set; } |
||||
|
||||
[XmlElement("Part")] |
||||
public List<T> Part { get; set; } |
||||
} |
||||
} |
||||
|
||||
@ -1,26 +1,74 @@
@@ -1,26 +1,74 @@
|
||||
using System.Collections.Generic; |
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
{ |
||||
public class PlexMetadataResponse |
||||
{ |
||||
[XmlAttribute("key")] |
||||
public string Key { get; set; } |
||||
|
||||
[XmlAttribute("title")] |
||||
public string Title { get; set; } |
||||
|
||||
[XmlAttribute("contentRating")] |
||||
public string ContentRating { get; set; } |
||||
|
||||
[XmlAttribute("summary")] |
||||
public string Summary { get; set; } |
||||
|
||||
[XmlAttribute("year")] |
||||
public int Year { get; set; } |
||||
|
||||
[XmlAttribute("tagline")] |
||||
public string Tagline { get; set; } |
||||
|
||||
[XmlAttribute("thumb")] |
||||
public string Thumb { get; set; } |
||||
|
||||
[XmlAttribute("art")] |
||||
public string Art { get; set; } |
||||
|
||||
[XmlAttribute("originallyAvailableAt")] |
||||
public string OriginallyAvailableAt { get; set; } |
||||
|
||||
[XmlAttribute("addedAt")] |
||||
public long AddedAt { get; set; } |
||||
|
||||
[XmlAttribute("updatedAt")] |
||||
public long UpdatedAt { get; set; } |
||||
|
||||
[XmlAttribute("index")] |
||||
public int Index { get; set; } |
||||
|
||||
[XmlAttribute("studio")] |
||||
public string Studio { get; set; } |
||||
public List<PlexMediaResponse> Media { get; set; } |
||||
|
||||
[XmlAttribute("rating")] |
||||
public double Rating { get; set; } |
||||
|
||||
[XmlAttribute("audienceRating")] |
||||
public double AudienceRating { get; set; } |
||||
|
||||
[XmlAttribute("audienceRatingImage")] |
||||
public string AudienceRatingImage { get; set; } |
||||
|
||||
[XmlAttribute("ratingImage")] |
||||
public string RatingImage { get; set; } |
||||
|
||||
[XmlIgnore] |
||||
public virtual List<PlexMediaResponse<PlexPartResponse>> Media { get; set; } |
||||
|
||||
[XmlElement("Genre")] |
||||
public List<PlexGenreResponse> Genre { get; set; } |
||||
|
||||
[XmlElement("Role")] |
||||
public List<PlexRoleResponse> Role { get; set; } |
||||
|
||||
[XmlElement("Director")] |
||||
public List<PlexDirectorResponse> Director { get; set; } |
||||
|
||||
[XmlElement("Writer")] |
||||
public List<PlexWriterResponse> Writer { get; set; } |
||||
} |
||||
} |
||||
|
||||
@ -1,9 +1,16 @@
@@ -1,9 +1,16 @@
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
{ |
||||
public class PlexRoleResponse |
||||
{ |
||||
[XmlAttribute("tag")] |
||||
public string Tag { get; set; } |
||||
|
||||
[XmlAttribute("role")] |
||||
public string Role { get; set; } |
||||
|
||||
[XmlAttribute("thumb")] |
||||
public string Thumb { get; set; } |
||||
} |
||||
} |
||||
|
||||
@ -1,18 +1,43 @@
@@ -1,18 +1,43 @@
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
{ |
||||
public class PlexStreamResponse |
||||
{ |
||||
[XmlAttribute("id")] |
||||
public int Id { get; set; } |
||||
|
||||
[XmlAttribute("index")] |
||||
public int Index { get; set; } |
||||
|
||||
[XmlAttribute("default")] |
||||
public bool Default { get; set; } |
||||
|
||||
[XmlAttribute("forced")] |
||||
public bool Forced { get; set; } |
||||
|
||||
[XmlAttribute("languageCode")] |
||||
public string LanguageCode { get; set; } |
||||
|
||||
[XmlAttribute("streamType")] |
||||
public int StreamType { get; set; } |
||||
|
||||
[XmlAttribute("codec")] |
||||
public string Codec { get; set; } |
||||
|
||||
[XmlAttribute("profile")] |
||||
public string Profile { get; set; } |
||||
|
||||
[XmlAttribute("channels")] |
||||
public int Channels { get; set; } |
||||
|
||||
[XmlAttribute("anamorphic")] |
||||
public bool Anamorphic { get; set; } |
||||
|
||||
[XmlAttribute("pixelAspectRatio")] |
||||
public string PixelAspectRatio { get; set; } |
||||
|
||||
[XmlAttribute("scanType")] |
||||
public string ScanType { get; set; } |
||||
} |
||||
} |
||||
|
||||
@ -1,7 +1,10 @@
@@ -1,7 +1,10 @@
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
{ |
||||
public class PlexWriterResponse |
||||
{ |
||||
[XmlAttribute("tag")] |
||||
public string Tag { get; set; } |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic; |
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
{ |
||||
public class PlexXmlMetadataResponse : PlexMetadataResponse |
||||
{ |
||||
[XmlAttribute("guid")] |
||||
public string PlexGuid { get; set; } |
||||
|
||||
[XmlElement("Media")] |
||||
public new List<PlexMediaResponse<PlexXmlPartResponse>> Media { get; set; } |
||||
|
||||
[XmlElement("Guid")] |
||||
public List<PlexGuidResponse> Guid { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic; |
||||
using System.Xml.Serialization; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Plex.Models |
||||
{ |
||||
public class PlexXmlPartResponse |
||||
{ |
||||
[XmlAttribute("id")] |
||||
public int Id { get; set; } |
||||
|
||||
[XmlAttribute("key")] |
||||
public string Key { get; set; } |
||||
|
||||
[XmlAttribute("duration")] |
||||
public int Duration { get; set; } |
||||
|
||||
[XmlAttribute("file")] |
||||
public string File { get; set; } |
||||
|
||||
[XmlElement("Stream")] |
||||
public List<PlexStreamResponse> Stream { get; set; } |
||||
} |
||||
} |
||||
@ -0,0 +1,166 @@
@@ -0,0 +1,166 @@
|
||||
@page "/media/tv/episodes" |
||||
@page "/media/tv/episodes/page/{PageNumber:int}" |
||||
@using LanguageExt.UnsafeValueAccess |
||||
@using Microsoft.AspNetCore.WebUtilities |
||||
@using Microsoft.Extensions.Primitives |
||||
@using ErsatzTV.Application.MediaCards |
||||
@using ErsatzTV.Application.MediaCollections |
||||
@using ErsatzTV.Application.MediaCollections.Commands |
||||
@using ErsatzTV.Application.Search.Queries |
||||
@using Unit = LanguageExt.Unit |
||||
@inherits MultiSelectBase<EpisodeList> |
||||
@inject NavigationManager _navigationManager |
||||
@inject ChannelWriter<IBackgroundServiceRequest> _channel |
||||
|
||||
<MudPaper Square="true" Style="display: flex; height: 64px; left: 240px; padding: 0; position: fixed; right: 0; z-index: 100;"> |
||||
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%" class="ml-6 mr-6"> |
||||
@if (IsSelectMode()) |
||||
{ |
||||
<MudText Typo="Typo.h6" Color="Color.Primary">@SelectionLabel()</MudText> |
||||
<div style="margin-left: auto"> |
||||
<MudButton Variant="Variant.Filled" |
||||
Color="Color.Primary" |
||||
StartIcon="@Icons.Material.Filled.Add" |
||||
OnClick="@(_ => AddSelectionToCollection())"> |
||||
Add To Collection |
||||
</MudButton> |
||||
<MudButton Class="ml-3" |
||||
Variant="Variant.Filled" |
||||
Color="Color.Secondary" |
||||
StartIcon="@Icons.Material.Filled.Check" |
||||
OnClick="@(_ => ClearSelection())"> |
||||
Clear Selection |
||||
</MudButton> |
||||
</div> |
||||
} |
||||
else |
||||
{ |
||||
<MudText Style="margin-bottom: auto; margin-top: auto; width: 33%">@_query</MudText> |
||||
<div style="max-width: 300px; width: 33%;"> |
||||
<MudPaper Style="align-items: center; display: flex; justify-content: center;"> |
||||
<MudIconButton Icon="@Icons.Material.Outlined.ChevronLeft" |
||||
OnClick="@PrevPage" |
||||
Disabled="@(PageNumber <= 1)"> |
||||
</MudIconButton> |
||||
<MudText Style="flex-grow: 1" |
||||
Align="Align.Center"> |
||||
@Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count |
||||
</MudText> |
||||
<MudIconButton Icon="@Icons.Material.Outlined.ChevronRight" |
||||
OnClick="@NextPage" Disabled="@(PageNumber * PageSize >= _data.Count)"> |
||||
</MudIconButton> |
||||
</MudPaper> |
||||
</div> |
||||
} |
||||
</div> |
||||
</MudPaper> |
||||
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8" Style="margin-top: 64px"> |
||||
<MudContainer MaxWidth="MaxWidth.False" Class="media-card-grid"> |
||||
<FragmentLetterAnchor TCard="TelevisionEpisodeCardViewModel" Cards="@_data.Cards"> |
||||
<MediaCard Data="@context" |
||||
Link="@($"/media/tv/seasons/{context.SeasonId}#episode-{context.EpisodeId}")" |
||||
Subtitle="@($"{context.ShowTitle} - S{context.Season} E{context.Episode}")" |
||||
AddToCollectionClicked="@AddToCollection" |
||||
SelectClicked="@(e => SelectClicked(context, e))" |
||||
IsSelected="@IsSelected(context)" |
||||
IsSelectMode="@IsSelectMode()"/> |
||||
</FragmentLetterAnchor> |
||||
</MudContainer> |
||||
</MudContainer> |
||||
@if (_data.PageMap.IsSome) |
||||
{ |
||||
<LetterBar PageMap="@_data.PageMap.ValueUnsafe()" |
||||
BaseUri="/media/tv/episodes" |
||||
Query="@_query"/> |
||||
} |
||||
|
||||
@code { |
||||
private static int PageSize => 100; |
||||
|
||||
[Parameter] |
||||
public int PageNumber { get; set; } |
||||
|
||||
private TelevisionEpisodeCardResultsViewModel _data; |
||||
private string _query; |
||||
|
||||
protected override Task OnParametersSetAsync() |
||||
{ |
||||
if (PageNumber == 0) |
||||
{ |
||||
PageNumber = 1; |
||||
} |
||||
|
||||
string query = new Uri(_navigationManager.Uri).Query; |
||||
if (QueryHelpers.ParseQuery(query).TryGetValue("query", out StringValues value)) |
||||
{ |
||||
_query = value; |
||||
} |
||||
else |
||||
{ |
||||
_query = null; |
||||
} |
||||
|
||||
return RefreshData(); |
||||
} |
||||
|
||||
protected override async Task RefreshData() |
||||
{ |
||||
string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:episode" : $"type:episode AND ({_query})"; |
||||
_data = await Mediator.Send(new QuerySearchIndexEpisodes(searchQuery, PageNumber, PageSize)); |
||||
} |
||||
|
||||
private void PrevPage() |
||||
{ |
||||
var uri = $"/media/tv/episodes/page/{PageNumber - 1}"; |
||||
if (!string.IsNullOrWhiteSpace(_query)) |
||||
{ |
||||
uri = QueryHelpers.AddQueryString(uri, "query", _query); |
||||
} |
||||
_navigationManager.NavigateTo(uri); |
||||
} |
||||
|
||||
private void NextPage() |
||||
{ |
||||
var uri = $"/media/tv/episodes/page/{PageNumber + 1}"; |
||||
if (!string.IsNullOrWhiteSpace(_query)) |
||||
{ |
||||
uri = QueryHelpers.AddQueryString(uri, "query", _query); |
||||
} |
||||
_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 TelevisionEpisodeCardViewModel episode) |
||||
{ |
||||
var parameters = new DialogParameters { { "EntityType", "episode" }, { "EntityName", episode.Title } }; |
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; |
||||
|
||||
IDialogReference dialog = Dialog.Show<AddToCollectionDialog>("Add To Collection", parameters, options); |
||||
DialogResult result = await dialog.Result; |
||||
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) |
||||
{ |
||||
var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId); |
||||
Either<BaseError, Unit> addResult = await Mediator.Send(request); |
||||
addResult.Match( |
||||
Left: error => |
||||
{ |
||||
Snackbar.Add($"Unexpected error adding episode to collection: {error.Value}"); |
||||
Logger.LogError("Unexpected error adding episode to collection: {Error}", error.Value); |
||||
}, |
||||
Right: _ => Snackbar.Add($"Added {episode.Title} to collection {collection.Name}", Severity.Success)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue