add jellyfin media source (#185)

* wip

* start to add jellyfin tables to db

* code cleanup

* finish adding jellyfin media source

* sync jellyfin libraries

* display list of jellyfin libraries

* toggle jellyfin library sync

* edit jellyfin path replacements

* noop jellyfin scanners

* get jellyfin admin user id on startup

* implement jellyfin disconnect

* add jellyfin libraries to list; start to query jellyfin library items

* code cleanup

* start to project jellyfin movies

* save new jellyfin movies to db

* basic jellyfin movie update

* load jellyfin actor artwork

* load jellyfin movie poster and fan art

* more jellyfin artwork fixes, sync audio streams

* jellyfin playback sort of works

* skip jellyfin movies that are inaccessible

* use ffprobe for jellyfin movie statistics

* code cleanup

* store jellyfin operating system

* more jellyfin movie updates

* update jellyfin movie poster and fan art

* add jellyfin tv types

* sync jellyfin shows

* sync jellyfin seasons

* sync jellyfin episodes

* remove missing jellyfin television items

* delete empty jellyfin seasons and shows

* fix jellyfin updates

* fix indexing jellyfin movie and show languages
This commit is contained in:
Jason Dove
2021-05-15 13:14:17 -05:00
committed by GitHub
parent 27e0a70d93
commit 4d86250630
132 changed files with 17318 additions and 106 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,92 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_JellyfinMediaSource : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
"JellyfinMediaSource",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ServerName = table.Column<string>("TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_JellyfinMediaSource", x => x.Id);
table.ForeignKey(
"FK_JellyfinMediaSource_MediaSource_Id",
x => x.Id,
"MediaSource",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"JellyfinConnection",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Address = table.Column<string>("TEXT", nullable: true),
JellyfinMediaSourceId = table.Column<int>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_JellyfinConnection", x => x.Id);
table.ForeignKey(
"FK_JellyfinConnection_JellyfinMediaSource_JellyfinMediaSourceId",
x => x.JellyfinMediaSourceId,
"JellyfinMediaSource",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"JellyfinPathReplacement",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
JellyfinPath = table.Column<string>("TEXT", nullable: true),
LocalPath = table.Column<string>("TEXT", nullable: true),
JellyfinMediaSourceId = table.Column<int>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_JellyfinPathReplacement", x => x.Id);
table.ForeignKey(
"FK_JellyfinPathReplacement_JellyfinMediaSource_JellyfinMediaSourceId",
x => x.JellyfinMediaSourceId,
"JellyfinMediaSource",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
"IX_JellyfinConnection_JellyfinMediaSourceId",
"JellyfinConnection",
"JellyfinMediaSourceId");
migrationBuilder.CreateIndex(
"IX_JellyfinPathReplacement_JellyfinMediaSourceId",
"JellyfinPathReplacement",
"JellyfinMediaSourceId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
"JellyfinConnection");
migrationBuilder.DropTable(
"JellyfinPathReplacement");
migrationBuilder.DropTable(
"JellyfinMediaSource");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_JellyfinLibrary : Migration
{
protected override void Up(MigrationBuilder migrationBuilder) =>
migrationBuilder.CreateTable(
"JellyfinLibrary",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ItemId = table.Column<string>("TEXT", nullable: true),
ShouldSyncItems = table.Column<bool>("INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_JellyfinLibrary", x => x.Id);
table.ForeignKey(
"FK_JellyfinLibrary_Library_Id",
x => x.Id,
"Library",
"Id",
onDelete: ReferentialAction.Cascade);
});
protected override void Down(MigrationBuilder migrationBuilder) =>
migrationBuilder.DropTable(
"JellyfinLibrary");
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_JellyfinMovie : Migration
{
protected override void Up(MigrationBuilder migrationBuilder) =>
migrationBuilder.CreateTable(
"JellyfinMovie",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ItemId = table.Column<string>("TEXT", nullable: true),
Etag = table.Column<string>("TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_JellyfinMovie", x => x.Id);
table.ForeignKey(
"FK_JellyfinMovie_Movie_Id",
x => x.Id,
"Movie",
"Id",
onDelete: ReferentialAction.Cascade);
});
protected override void Down(MigrationBuilder migrationBuilder) =>
migrationBuilder.DropTable(
"JellyfinMovie");
}
}
@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_JellyfinMediaSourceOperatingSystem : Migration
{
protected override void Up(MigrationBuilder migrationBuilder) =>
migrationBuilder.AddColumn<string>(
"OperatingSystem",
"JellyfinMediaSource",
"TEXT",
nullable: true);
protected override void Down(MigrationBuilder migrationBuilder) =>
migrationBuilder.DropColumn(
"OperatingSystem",
"JellyfinMediaSource");
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,82 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_JellyfinShowSeasonEpisode : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
"JellyfinEpisode",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ItemId = table.Column<string>("TEXT", nullable: true),
Etag = table.Column<string>("TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_JellyfinEpisode", x => x.Id);
table.ForeignKey(
"FK_JellyfinEpisode_Episode_Id",
x => x.Id,
"Episode",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"JellyfinSeason",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ItemId = table.Column<string>("TEXT", nullable: true),
Etag = table.Column<string>("TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_JellyfinSeason", x => x.Id);
table.ForeignKey(
"FK_JellyfinSeason_Season_Id",
x => x.Id,
"Season",
"Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"JellyfinShow",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ItemId = table.Column<string>("TEXT", nullable: true),
Etag = table.Column<string>("TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_JellyfinShow", x => x.Id);
table.ForeignKey(
"FK_JellyfinShow_Show_Id",
x => x.Id,
"Show",
"Id",
onDelete: ReferentialAction.Cascade);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
"JellyfinEpisode");
migrationBuilder.DropTable(
"JellyfinSeason");
migrationBuilder.DropTable(
"JellyfinShow");
}
}
}
@@ -445,6 +445,51 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("Genre");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinConnection",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Address")
.HasColumnType("TEXT");
b.Property<int>("JellyfinMediaSourceId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("JellyfinMediaSourceId");
b.ToTable("JellyfinConnection");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinPathReplacement",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("JellyfinMediaSourceId")
.HasColumnType("INTEGER");
b.Property<string>("JellyfinPath")
.HasColumnType("TEXT");
b.Property<string>("LocalPath")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("JellyfinMediaSourceId");
b.ToTable("JellyfinPathReplacement");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Library",
b =>
@@ -1209,6 +1254,21 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("Tag");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinLibrary",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Library");
b.Property<string>("ItemId")
.HasColumnType("TEXT");
b.Property<bool>("ShouldSyncItems")
.HasColumnType("INTEGER");
b.ToTable("JellyfinLibrary");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.LocalLibrary",
b =>
@@ -1323,6 +1383,21 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("Show");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinMediaSource",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.MediaSource");
b.Property<string>("OperatingSystem")
.HasColumnType("TEXT");
b.Property<string>("ServerName")
.HasColumnType("TEXT");
b.ToTable("JellyfinMediaSource");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.LocalMediaSource",
b =>
@@ -1401,6 +1476,21 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("ProgramScheduleOneItem");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinEpisode",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Episode");
b.Property<string>("Etag")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasColumnType("TEXT");
b.ToTable("JellyfinEpisode");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexEpisode",
b =>
@@ -1413,6 +1503,21 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("PlexEpisode");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinMovie",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Movie");
b.Property<string>("Etag")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasColumnType("TEXT");
b.ToTable("JellyfinMovie");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexMovie",
b =>
@@ -1425,6 +1530,21 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("PlexMovie");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinSeason",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Season");
b.Property<string>("Etag")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasColumnType("TEXT");
b.ToTable("JellyfinSeason");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexSeason",
b =>
@@ -1437,6 +1557,21 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("PlexSeason");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinShow",
b =>
{
b.HasBaseType("ErsatzTV.Core.Domain.Show");
b.Property<string>("Etag")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasColumnType("TEXT");
b.ToTable("JellyfinShow");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexShow",
b =>
@@ -1634,6 +1769,32 @@ namespace ErsatzTV.Infrastructure.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinConnection",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.JellyfinMediaSource", "JellyfinMediaSource")
.WithMany("Connections")
.HasForeignKey("JellyfinMediaSourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("JellyfinMediaSource");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinPathReplacement",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.JellyfinMediaSource", "JellyfinMediaSource")
.WithMany("PathReplacements")
.HasForeignKey("JellyfinMediaSourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("JellyfinMediaSource");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.Library",
b =>
@@ -2049,6 +2210,17 @@ namespace ErsatzTV.Infrastructure.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinLibrary",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.Library", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinLibrary", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.LocalLibrary",
b =>
@@ -2172,6 +2344,17 @@ namespace ErsatzTV.Infrastructure.Migrations
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinMediaSource",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.MediaSource", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinMediaSource", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.LocalMediaSource",
b =>
@@ -2238,6 +2421,17 @@ namespace ErsatzTV.Infrastructure.Migrations
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinEpisode",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.Episode", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinEpisode", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexEpisode",
b =>
@@ -2249,6 +2443,17 @@ namespace ErsatzTV.Infrastructure.Migrations
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinMovie",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.Movie", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinMovie", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexMovie",
b =>
@@ -2260,6 +2465,17 @@ namespace ErsatzTV.Infrastructure.Migrations
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinSeason",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.Season", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinSeason", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexSeason",
b =>
@@ -2271,6 +2487,17 @@ namespace ErsatzTV.Infrastructure.Migrations
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinShow",
b =>
{
b.HasOne("ErsatzTV.Core.Domain.Show", null)
.WithOne()
.HasForeignKey("ErsatzTV.Core.Domain.JellyfinShow", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexShow",
b =>
@@ -2476,6 +2703,15 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("ShowMetadata");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.JellyfinMediaSource",
b =>
{
b.Navigation("Connections");
b.Navigation("PathReplacements");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.PlexMediaSource",
b =>