Browse Source

fix mixed platform directory mapping (#106)

* sync plex platform and platform version

* fix mixed-platform path replacements
pull/107/head
Jason Dove 5 years ago committed by GitHub
parent
commit
104d4a0cbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs
  2. 43
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  3. 147
      ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs
  4. 2
      ErsatzTV.Core/Domain/MediaSource/PlexMediaSource.cs
  5. 9
      ErsatzTV.Core/Interfaces/Plex/IPlexPathReplacementService.cs
  6. 11
      ErsatzTV.Core/Interfaces/Runtime/IRuntimeInfo.cs
  7. 67
      ErsatzTV.Core/Plex/PlexPathReplacementService.cs
  8. 17
      ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs
  9. 1755
      ErsatzTV.Infrastructure/Migrations/20210327234641_Add_PlexMediaSourcePlatform.Designer.cs
  10. 33
      ErsatzTV.Infrastructure/Migrations/20210327234641_Add_PlexMediaSourcePlatform.cs
  11. 6
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  12. 2
      ErsatzTV.Infrastructure/Plex/Models/PlexResource.cs
  13. 2
      ErsatzTV.Infrastructure/Plex/PlexTvApiClient.cs
  14. 10
      ErsatzTV.Infrastructure/Runtime/RuntimeInfo.cs
  15. 4
      ErsatzTV/Startup.cs

2
ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs

@ -64,6 +64,8 @@ namespace ErsatzTV.Application.Plex.Commands @@ -64,6 +64,8 @@ namespace ErsatzTV.Application.Plex.Commands
await maybeExisting.Match(
existing =>
{
existing.Platform = server.Platform;
existing.PlatformVersion = server.PlatformVersion;
existing.ProductVersion = server.ProductVersion;
existing.ServerName = server.ServerName;
var toAdd = server.Connections

43
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -1,17 +1,14 @@ @@ -1,17 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Streaming.Queries
@ -22,26 +19,23 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -22,26 +19,23 @@ namespace ErsatzTV.Application.Streaming.Queries
private readonly IConfigElementRepository _configElementRepository;
private readonly FFmpegProcessService _ffmpegProcessService;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<GetPlayoutItemProcessByChannelNumberHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IPlayoutRepository _playoutRepository;
private readonly IPlexPathReplacementService _plexPathReplacementService;
public GetPlayoutItemProcessByChannelNumberHandler(
IChannelRepository channelRepository,
IConfigElementRepository configElementRepository,
IPlayoutRepository playoutRepository,
IMediaSourceRepository mediaSourceRepository,
FFmpegProcessService ffmpegProcessService,
ILocalFileSystem localFileSystem,
ILogger<GetPlayoutItemProcessByChannelNumberHandler> logger)
IPlexPathReplacementService plexPathReplacementService)
: base(channelRepository, configElementRepository)
{
_configElementRepository = configElementRepository;
_playoutRepository = playoutRepository;
_mediaSourceRepository = mediaSourceRepository;
_ffmpegProcessService = ffmpegProcessService;
_localFileSystem = localFileSystem;
_logger = logger;
_plexPathReplacementService = plexPathReplacementService;
}
protected override async Task<Either<BaseError, Process>> GetProcess(
@ -166,33 +160,16 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -166,33 +160,16 @@ namespace ErsatzTV.Application.Streaming.Queries
string path = file.Path;
return playoutItem.MediaItem switch
{
PlexMovie plexMovie => await GetReplacementPlexPath(plexMovie.LibraryPathId, path),
PlexEpisode plexEpisode => await GetReplacementPlexPath(plexEpisode.LibraryPathId, path),
PlexMovie plexMovie => await _plexPathReplacementService.GetReplacementPlexPath(
plexMovie.LibraryPathId,
path),
PlexEpisode plexEpisode => await _plexPathReplacementService.GetReplacementPlexPath(
plexEpisode.LibraryPathId,
path),
_ => path
};
}
private async Task<string> GetReplacementPlexPath(int libraryPathId, string path)
{
List<PlexPathReplacement> replacements =
await _mediaSourceRepository.GetPlexPathReplacementsByLibraryId(libraryPathId);
// TODO: this might barf mixing platforms (i.e. plex on linux, etv on windows)
Option<PlexPathReplacement> maybeReplacement = replacements
.SingleOrDefault(r => path.StartsWith(r.PlexPath + Path.DirectorySeparatorChar));
return maybeReplacement.Match(
replacement =>
{
string finalPath = path.Replace(replacement.PlexPath, replacement.LocalPath);
_logger.LogInformation(
"Replacing plex path {PlexPath} with {LocalPath} resulting in {FinalPath}",
replacement.PlexPath,
replacement.LocalPath,
finalPath);
return finalPath;
},
() => path);
}
private record PlayoutItemWithPath(PlayoutItem PlayoutItem, string Path);
}
}

147
ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs

@ -0,0 +1,147 @@ @@ -0,0 +1,147 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.Core.Plex;
using FluentAssertions;
using LanguageExt;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
namespace ErsatzTV.Core.Tests.Plex
{
[TestFixture]
public class PlexPathReplacementServiceTests
{
[Test]
public async Task PlexWindows_To_EtvWindows()
{
var replacements = new List<PlexPathReplacement>
{
new()
{
Id = 1,
PlexPath = @"C:\Something\Some Shared Folder",
LocalPath = @"C:\Something Else\Some Shared Folder",
PlexMediaSource = new PlexMediaSource { Platform = "Windows" }
}
};
var repo = new Mock<IMediaSourceRepository>();
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
var runtime = new Mock<IRuntimeInfo>();
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true);
var service = new PlexPathReplacementService(
repo.Object,
runtime.Object,
new Mock<ILogger<PlexPathReplacementService>>().Object);
string result = await service.GetReplacementPlexPath(
0,
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
}
[Test]
public async Task PlexWindows_To_EtvLinux()
{
var replacements = new List<PlexPathReplacement>
{
new()
{
Id = 1,
PlexPath = @"C:\Something\Some Shared Folder",
LocalPath = @"/mnt/something else/Some Shared Folder",
PlexMediaSource = new PlexMediaSource { Platform = "Windows" }
}
};
var repo = new Mock<IMediaSourceRepository>();
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
var runtime = new Mock<IRuntimeInfo>();
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
var service = new PlexPathReplacementService(
repo.Object,
runtime.Object,
new Mock<ILogger<PlexPathReplacementService>>().Object);
string result = await service.GetReplacementPlexPath(
0,
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
}
[Test]
public async Task PlexLinux_To_EtvWindows()
{
var replacements = new List<PlexPathReplacement>
{
new()
{
Id = 1,
PlexPath = @"/mnt/something/Some Shared Folder",
LocalPath = @"C:\Something Else\Some Shared Folder",
PlexMediaSource = new PlexMediaSource { Platform = "Linux" }
}
};
var repo = new Mock<IMediaSourceRepository>();
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
var runtime = new Mock<IRuntimeInfo>();
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true);
var service = new PlexPathReplacementService(
repo.Object,
runtime.Object,
new Mock<ILogger<PlexPathReplacementService>>().Object);
string result = await service.GetReplacementPlexPath(
0,
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
}
[Test]
public async Task PlexLinux_To_EtvLinux()
{
var replacements = new List<PlexPathReplacement>
{
new()
{
Id = 1,
PlexPath = @"/mnt/something/Some Shared Folder",
LocalPath = @"/mnt/something else/Some Shared Folder",
PlexMediaSource = new PlexMediaSource { Platform = "Linux" }
}
};
var repo = new Mock<IMediaSourceRepository>();
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
var runtime = new Mock<IRuntimeInfo>();
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
var service = new PlexPathReplacementService(
repo.Object,
runtime.Object,
new Mock<ILogger<PlexPathReplacementService>>().Object);
string result = await service.GetReplacementPlexPath(
0,
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
}
}
}

2
ErsatzTV.Core/Domain/MediaSource/PlexMediaSource.cs

@ -6,6 +6,8 @@ namespace ErsatzTV.Core.Domain @@ -6,6 +6,8 @@ namespace ErsatzTV.Core.Domain
{
public string ServerName { get; set; }
public string ProductVersion { get; set; }
public string Platform { get; set; }
public string PlatformVersion { get; set; }
public string ClientIdentifier { get; set; }
// public bool IsOwned { get; set; }

9
ErsatzTV.Core/Interfaces/Plex/IPlexPathReplacementService.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace ErsatzTV.Core.Interfaces.Plex
{
public interface IPlexPathReplacementService
{
Task<string> GetReplacementPlexPath(int libraryPathId, string path);
}
}

11
ErsatzTV.Core/Interfaces/Runtime/IRuntimeInfo.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace ErsatzTV.Core.Interfaces.Runtime
{
public interface IRuntimeInfo
{
[SuppressMessage("ReSharper", "InconsistentNaming")]
bool IsOSPlatform(OSPlatform osPlatform);
}
}

67
ErsatzTV.Core/Plex/PlexPathReplacementService.cs

@ -0,0 +1,67 @@ @@ -0,0 +1,67 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime;
using LanguageExt;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Plex
{
public class PlexPathReplacementService : IPlexPathReplacementService
{
private readonly ILogger<PlexPathReplacementService> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IRuntimeInfo _runtimeInfo;
public PlexPathReplacementService(
IMediaSourceRepository mediaSourceRepository,
IRuntimeInfo runtimeInfo,
ILogger<PlexPathReplacementService> logger)
{
_mediaSourceRepository = mediaSourceRepository;
_runtimeInfo = runtimeInfo;
_logger = logger;
}
public async Task<string> GetReplacementPlexPath(int libraryPathId, string path)
{
List<PlexPathReplacement> replacements =
await _mediaSourceRepository.GetPlexPathReplacementsByLibraryId(libraryPathId);
Option<PlexPathReplacement> maybeReplacement = replacements
.SingleOrDefault(
r =>
{
string separatorChar = IsWindows(r.PlexMediaSource) ? @"\" : @"/";
return path.StartsWith(r.PlexPath + separatorChar);
});
return maybeReplacement.Match(
replacement =>
{
string finalPath = path.Replace(replacement.PlexPath, replacement.LocalPath);
if (IsWindows(replacement.PlexMediaSource) && !_runtimeInfo.IsOSPlatform(OSPlatform.Windows))
{
finalPath = finalPath.Replace(@"\", @"/");
}
else if (!IsWindows(replacement.PlexMediaSource) && _runtimeInfo.IsOSPlatform(OSPlatform.Windows))
{
finalPath = finalPath.Replace(@"/", @"\");
}
_logger.LogInformation(
"Replacing plex path {PlexPath} with {LocalPath} resulting in {FinalPath}",
replacement.PlexPath,
replacement.LocalPath,
finalPath);
return finalPath;
},
() => path);
}
private static bool IsWindows(PlexMediaSource plexMediaSource) =>
plexMediaSource.Platform.ToLowerInvariant() == "windows";
}
}

17
ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs

@ -135,6 +135,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -135,6 +135,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
inner join PlexPathReplacement ppr on ppr.PlexMediaSourceId = l.MediaSourceId
where lp.Id = {0}",
plexLibraryPathId)
.Include(ppr => ppr.PlexMediaSource)
.ToListAsync();
}
@ -158,8 +159,20 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -158,8 +159,20 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
List<PlexConnection> toDelete)
{
await _dbConnection.ExecuteAsync(
@"UPDATE PlexMediaSource SET ProductVersion = @ProductVersion, ServerName = @ServerName WHERE Id = @Id",
new { plexMediaSource.ProductVersion, plexMediaSource.ServerName, plexMediaSource.Id });
@"UPDATE PlexMediaSource SET
ProductVersion = @ProductVersion,
Platform = @Platform,
PlatformVersion = @PlatformVersion,
ServerName = @ServerName
WHERE Id = @Id",
new
{
plexMediaSource.ProductVersion,
plexMediaSource.Platform,
plexMediaSource.PlatformVersion,
plexMediaSource.ServerName,
plexMediaSource.Id
});
await using TvContext dbContext = _dbContextFactory.CreateDbContext();

1755
ErsatzTV.Infrastructure/Migrations/20210327234641_Add_PlexMediaSourcePlatform.Designer.cs generated

File diff suppressed because it is too large Load Diff

33
ErsatzTV.Infrastructure/Migrations/20210327234641_Add_PlexMediaSourcePlatform.cs

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_PlexMediaSourcePlatform : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
"Platform",
"PlexMediaSource",
"TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
"PlatformVersion",
"PlexMediaSource",
"TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
"Platform",
"PlexMediaSource");
migrationBuilder.DropColumn(
"PlatformVersion",
"PlexMediaSource");
}
}
}

6
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -1026,6 +1026,12 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -1026,6 +1026,12 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("ClientIdentifier")
.HasColumnType("TEXT");
b.Property<string>("Platform")
.HasColumnType("TEXT");
b.Property<string>("PlatformVersion")
.HasColumnType("TEXT");
b.Property<string>("ProductVersion")
.HasColumnType("TEXT");

2
ErsatzTV.Infrastructure/Plex/Models/PlexResource.cs

@ -6,6 +6,8 @@ namespace ErsatzTV.Infrastructure.Plex.Models @@ -6,6 +6,8 @@ namespace ErsatzTV.Infrastructure.Plex.Models
{
public string Name { get; set; }
public string ProductVersion { get; set; }
public string Platform { get; set; }
public string PlatformVersion { get; set; }
public string ClientIdentifier { get; set; }
public string AccessToken { get; set; }

2
ErsatzTV.Infrastructure/Plex/PlexTvApiClient.cs

@ -70,6 +70,8 @@ namespace ErsatzTV.Infrastructure.Plex @@ -70,6 +70,8 @@ namespace ErsatzTV.Infrastructure.Plex
{
ServerName = resource.Name,
ProductVersion = resource.ProductVersion,
Platform = resource.Platform,
PlatformVersion = resource.PlatformVersion,
ClientIdentifier = resource.ClientIdentifier,
Connections = sortedConnections
.Map(c => new PlexConnection { Uri = c.Uri }).ToList()

10
ErsatzTV.Infrastructure/Runtime/RuntimeInfo.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
using ErsatzTV.Core.Interfaces.Runtime;
namespace ErsatzTV.Infrastructure.Runtime
{
public class RuntimeInfo : IRuntimeInfo
{
public bool IsOSPlatform(OSPlatform osPlatform) => RuntimeInformation.IsOSPlatform(osPlatform);
}
}

4
ErsatzTV/Startup.cs

@ -14,6 +14,7 @@ using ErsatzTV.Core.Interfaces.Locking; @@ -14,6 +14,7 @@ using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Runtime;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata;
@ -25,6 +26,7 @@ using ErsatzTV.Infrastructure.Data.Repositories; @@ -25,6 +26,7 @@ using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.Images;
using ErsatzTV.Infrastructure.Locking;
using ErsatzTV.Infrastructure.Plex;
using ErsatzTV.Infrastructure.Runtime;
using ErsatzTV.Infrastructure.Search;
using ErsatzTV.Serialization;
using ErsatzTV.Services;
@ -212,6 +214,8 @@ namespace ErsatzTV @@ -212,6 +214,8 @@ namespace ErsatzTV
services.AddScoped<IPlexTelevisionLibraryScanner, PlexTelevisionLibraryScanner>();
services.AddScoped<IPlexServerApiClient, PlexServerApiClient>();
services.AddScoped<ISearchIndex, SearchIndex>();
services.AddScoped<IRuntimeInfo, RuntimeInfo>();
services.AddScoped<IPlexPathReplacementService, PlexPathReplacementService>();
services.AddHostedService<PlexService>();
services.AddHostedService<FFmpegLocatorService>();

Loading…
Cancel
Save