Database redesign (#31)

* starting database redesign

* set season and episode numbers

* use datetimes in db (utc); update movie metadata

* get movie cards from new table

* copy show/episode metadata

* remove old movie metadata type

* rename new movie metadata type

* code cleanup

* start to remove old television classes

* remove old television tables from database

* fix playout building

* fix collection views

* fix show/season views

* clean up movie metadata table

* fix scanner tests

* add libraries ui

* code cleanup

* fix movie scanning/metadata

* add library scan button to ui

* delete library path from ui

* temp disable movie scanning

* remove orphan media items and prevent duplicate paths

* attach artwork to metadata

* fix split show/season display

* fix television artwork

* store year distinct from release date

* fix collections ui

* code cleanup

* add library paths from ui

* fix adding to collections from ui

* fix schedule items loading

* schedule editing works again

* remove some todos

* more cleanup

* fix unit tests

* fix episode sorting

* fix deleting show library paths

* remove unused class

* fix playout list in ui

* fix log viewer

* start to use version/file instead of statistics

* clean up old columns

* fix playout display (time zone)

* fix playback

* fix channel guide time zone

* cascade more deletes

* fix compiler warnings

* fix adding new seasons

* use artwork for channel logo

* clean cache folder on startup (move channel logos, delete everything else)

* log database migration

* update homepage docs for libraries

* fix adding new channel with logo

* fix episode numbers in epg
This commit is contained in:
Jason Dove
2021-02-28 17:48:01 +00:00
committed by GitHub
parent e25b9edd01
commit f392bab118
392 changed files with 52108 additions and 4079 deletions
@@ -8,8 +8,15 @@ namespace ErsatzTV.Infrastructure.Plex
public interface IPlexServerApi
{
[Get("/library/sections")]
public Task<PlexMediaContainerResponse<PlexLibraryResponse>> GetLibraries(
public Task<PlexMediaContainerResponse<PlexMediaContainerDirectoryContent<PlexLibraryResponse>>> GetLibraries(
[Query] [AliasAs("X-Plex-Token")]
string token);
[Get("/library/sections/{key}/all")]
public Task<PlexMediaContainerResponse<PlexMediaContainerMetadataContent<PlexMetadataResponse>>>
GetLibrarySectionContents(
string key,
[Query] [AliasAs("X-Plex-Token")]
string token);
}
}
@@ -4,11 +4,16 @@ namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexMediaContainerResponse<T>
{
public PlexMediaContainerContent<T> MediaContainer { get; set; }
public T MediaContainer { get; set; }
}
public class PlexMediaContainerContent<T>
public class PlexMediaContainerDirectoryContent<T>
{
public List<T> Directory { get; set; }
}
public class PlexMediaContainerMetadataContent<T>
{
public List<T> Metadata { get; set; }
}
}
@@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexMediaResponse
{
public int Id { get; set; }
public int Duration { get; set; }
public int Bitrate { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public double AspectRatio { get; set; }
public int AudioChannels { get; set; }
public string AudioCodec { get; set; }
public string VideoCodec { get; set; }
public string Container { get; set; }
public string VideoFrameRate { get; set; }
public List<PlexPartResponse> Part { get; set; }
}
}
@@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexMetadataResponse
{
public string Key { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public int Year { get; set; }
public string Tagline { get; set; }
public string Thumb { get; set; }
public string Art { get; set; }
public string OriginallyAvailableAt { get; set; }
public int AddedAt { get; set; }
public int UpdatedAt { get; set; }
public List<PlexMediaResponse> Media { get; set; }
}
}
@@ -0,0 +1,11 @@
namespace ErsatzTV.Infrastructure.Plex.Models
{
public class PlexPartResponse
{
public int Id { get; set; }
public string Key { get; set; }
public int Duration { get; set; }
public string File { get; set; }
public int Size { get; set; }
}
}
@@ -4,8 +4,8 @@
{
public string Protocol { get; set; }
public string Address { get; set; }
public string Port { get; set; }
public int Port { get; set; }
public string Uri { get; set; }
public string Local { get; set; }
public bool Local { get; set; }
}
}
@@ -64,7 +64,14 @@ namespace ErsatzTV.Infrastructure.Plex
private static Task<PlexSecrets> ReadSecrets() =>
File.ReadAllTextAsync(FileSystemLayout.PlexSecretsPath)
.Map(JsonConvert.DeserializeObject<PlexSecrets>)
.Map(s => Optional(s).IfNone(new PlexSecrets()));
.Map(s => Optional(s).IfNone(new PlexSecrets()))
.Map(
s =>
{
s.ServerAuthTokens ??= new Dictionary<string, string>();
s.UserAuthTokens ??= new Dictionary<string, string>();
return s;
});
private static Task<Unit> SaveSecrets(PlexSecrets plexSecrets) =>
Some(JsonConvert.SerializeObject(plexSecrets)).Match(
@@ -15,8 +15,8 @@ namespace ErsatzTV.Infrastructure.Plex
{
public class PlexServerApiClient : IPlexServerApiClient
{
public async Task<Either<BaseError, List<PlexMediaSourceLibrary>>> GetLibraries(
PlexMediaSourceConnection connection,
public async Task<Either<BaseError, List<PlexLibrary>>> GetLibraries(
PlexConnection connection,
PlexServerAuthToken token)
{
try
@@ -36,23 +36,134 @@ namespace ErsatzTV.Infrastructure.Plex
}
}
private static Option<PlexMediaSourceLibrary> Project(PlexLibraryResponse response) =>
public async Task<Either<BaseError, List<PlexMovie>>> GetLibraryContents(
PlexLibrary library,
PlexConnection connection,
PlexServerAuthToken token)
{
try
{
IPlexServerApi service = RestService.For<IPlexServerApi>(connection.Uri);
return await service.GetLibrarySectionContents(library.Key, token.AuthToken)
.Map(r => r.MediaContainer.Metadata.Filter(m => m.Media.Count > 0 && m.Media[0].Part.Count > 0))
.Map(list => list.Map(ProjectToMovie).ToList());
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
private static Option<PlexLibrary> Project(PlexLibraryResponse response) =>
response.Type switch
{
"show" => new PlexMediaSourceLibrary
"show" => new PlexLibrary
{
Key = response.Key,
Name = response.Title,
MediaType = MediaType.TvShow
MediaKind = LibraryMediaKind.Shows,
ShouldSyncItems = false
},
"movie" => new PlexMediaSourceLibrary
"movie" => new PlexLibrary
{
Key = response.Key,
Name = response.Title,
MediaType = MediaType.Movie
MediaKind = LibraryMediaKind.Movies,
ShouldSyncItems = false
},
// TODO: "artist" for music libraries
_ => None
};
private static PlexPartEntry Project(PlexPartResponse response) =>
new()
{
Id = response.Id,
Key = response.Key,
Duration = response.Duration,
File = response.File,
Size = response.Size
};
private static PlexMediaEntry Project(PlexMediaResponse response) =>
new()
{
Id = response.Id,
Duration = response.Duration,
Bitrate = response.Bitrate,
Width = response.Width,
Height = response.Height,
AspectRatio = response.AspectRatio,
AudioChannels = response.AudioChannels,
AudioCodec = response.AudioCodec,
VideoCodec = response.VideoCodec,
Container = response.Container,
VideoFrameRate = response.VideoFrameRate,
Part = response.Part.Map(Project).ToList()
};
private static PlexMetadataEntry Project(PlexMetadataResponse response) =>
new()
{
Key = response.Key,
Title = response.Title,
Summary = response.Summary,
Year = response.Year,
Tagline = response.Tagline,
Thumb = response.Thumb,
Art = response.Art,
AddedAt = response.AddedAt,
UpdatedAt = response.UpdatedAt,
Media = response.Media.Map(Project).ToList()
};
private static PlexMovie ProjectToMovie(PlexMetadataResponse response)
{
PlexMediaResponse media = response.Media.Head();
PlexPartResponse part = media.Part.Head();
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
var metadata = new MovieMetadata
{
Title = response.Title,
Plot = response.Summary,
ReleaseDate = new DateTime(response.Year, 1, 1), // TODO: actual release date?
Year = response.Year,
Tagline = response.Tagline,
DateAdded = DateTime.UtcNow, // TODO: actual date added?
DateUpdated = lastWriteTime
};
// TODO: artwork
var movie = new PlexMovie
{
Key = response.Key,
// DateUpdated = lastWriteTime,
MovieMetadata = new List<MovieMetadata> { metadata },
// TODO: versions
// Statistics = new MediaItemStatistics
// {
// Duration = TimeSpan.FromMilliseconds(media.Duration),
// Width = media.Width,
// Height = media.Height,
// // TODO: aspect ratio
// AudioCodec = media.AudioCodec,
// VideoCodec = media.VideoCodec,
// LastWriteTime = lastWriteTime
// },
// TODO: multi-part movies?
Part = new PlexMediaItemPart
{
PlexId = part.Id,
Key = part.Key,
File = part.File,
Duration = part.Duration,
Size = part.Size
}
};
return movie;
}
}
}
@@ -49,11 +49,11 @@ namespace ErsatzTV.Infrastructure.Plex
var source = new PlexMediaSource
{
Name = resource.Name,
ServerName = resource.Name,
ProductVersion = resource.ProductVersion,
ClientIdentifier = resource.ClientIdentifier,
Connections = resource.Connections
.Map(c => new PlexMediaSourceConnection { Uri = c.Uri }).ToList()
.Map(c => new PlexConnection { Uri = c.Uri }).ToList()
};
return source;