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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user