Stream custom live channels using your own media
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

244 lines
9.8 KiB

using System.Collections.Immutable;
using System.Globalization;
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Infrastructure.Data.Repositories;
public class MediaItemRepository(
IDbContextFactory<TvContext> dbContextFactory,
ILanguageCodeService languageCodeService) : IMediaItemRepository
{
public async Task<List<CultureInfo>> GetAllKnownCultures()
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
var result = new System.Collections.Generic.HashSet<CultureInfo>();
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
foreach (LanguageCode code in await dbContext.LanguageCodes.ToListAsync())
{
Option<CultureInfo> maybeCulture = allCultures.Find(c =>
string.Equals(code.ThreeCode1, c.ThreeLetterISOLanguageName, StringComparison.OrdinalIgnoreCase)
|| string.Equals(
code.ThreeCode2,
c.ThreeLetterISOLanguageName,
StringComparison.OrdinalIgnoreCase));
foreach (CultureInfo culture in maybeCulture)
{
result.Add(culture);
}
}
return result.ToList();
}
public async Task<List<LanguageCodeAndName>> GetAllLanguageCodesAndNames()
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
var result = new System.Collections.Generic.HashSet<LanguageCodeAndName>();
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
List<string> mediaCodes = await GetAllLanguageCodes();
var unseenCodes = new System.Collections.Generic.HashSet<string>(mediaCodes);
foreach (string mediaCode in mediaCodes)
{
foreach (string code in languageCodeService.GetAllLanguageCodes(mediaCode))
{
Option<CultureInfo> maybeCulture = allCultures.Find(c => string.Equals(
code,
c.ThreeLetterISOLanguageName,
StringComparison.OrdinalIgnoreCase));
foreach (CultureInfo culture in maybeCulture)
{
unseenCodes.Remove(mediaCode);
unseenCodes.Remove(code);
result.Add(new LanguageCodeAndName(culture.ThreeLetterISOLanguageName, culture.EnglishName));
}
}
}
// every language code from the db must appear in the results
// entries that have no culture info (and thus english name) will just use the code twice
foreach (string mediaCode in unseenCodes.Where(c => !string.IsNullOrWhiteSpace(c)))
{
result.Add(new LanguageCodeAndName(mediaCode, mediaCode));
}
return result.ToList();
}
public async Task<List<int>> FlagFileNotFound(LibraryPath libraryPath, string path)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
List<int> ids = await dbContext.Connection.QueryAsync<int>(
@"SELECT M.Id
FROM MediaItem M
INNER JOIN MediaVersion MV on M.Id = COALESCE(MovieId, MusicVideoId, OtherVideoId, SongId, ImageId, RemoteStreamId, EpisodeId)
INNER JOIN MediaFile MF on MV.Id = MF.MediaVersionId
WHERE M.LibraryPathId = @LibraryPathId AND MF.Path = @Path",
new { LibraryPathId = libraryPath.Id, Path = path })
.Map(result => result.ToList());
await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids",
new { Ids = ids });
return ids;
}
public async Task<ImmutableHashSet<string>> GetAllTrashedItems(LibraryPath libraryPath)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<string>(
@"SELECT MF.Path
FROM MediaItem M
INNER JOIN MediaVersion MV on M.Id = COALESCE(MovieId, MusicVideoId, OtherVideoId, SongId, EpisodeId, ImageId, RemoteStreamId)
INNER JOIN MediaFile MF on MV.Id = MF.MediaVersionId
WHERE M.State IN (1,2) AND M.LibraryPathId = @LibraryPathId",
new { LibraryPathId = libraryPath.Id })
.Map(list => list.ToImmutableHashSet());
}
public async Task SetInterlacedRatio(MediaItem mediaItem, double interlacedRatio)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
var mediaVersion = mediaItem.GetHeadVersion();
mediaVersion.InterlacedRatio = interlacedRatio;
await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaVersion SET InterlacedRatio = @InterlacedRatio WHERE Id = @Id",
new { mediaVersion.Id, InterlacedRatio = interlacedRatio });
}
public async Task<Unit> FlagNormal(MediaItem mediaItem)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
mediaItem.State = MediaItemState.Normal;
return await dbContext.Connection.ExecuteAsync(
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id",
new { mediaItem.Id }).ToUnit();
}
public async Task<Either<BaseError, Unit>> DeleteItems(List<int> mediaItemIds)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
foreach (int mediaItemId in mediaItemIds)
{
await dbContext.Connection.ExecuteAsync(
"DELETE FROM MediaItem WHERE Id = @Id",
new { Id = mediaItemId });
}
return Unit.Default;
}
public static async Task<bool> MediaFileAlreadyExists(
MediaItem incoming,
int libraryPathId,
TvContext dbContext,
ILogger logger,
CancellationToken cancellationToken)
{
string path = incoming.GetHeadVersion().MediaFiles.Head().Path;
return await MediaFileAlreadyExists(path, libraryPathId, dbContext, logger, cancellationToken);
}
public static async Task<bool> MediaFileAlreadyExists(
string path,
int libraryPathId,
TvContext dbContext,
ILogger logger,
CancellationToken cancellationToken)
{
Option<int> maybeMediaItemId = await dbContext.Connection
.QuerySingleOrDefaultAsync<int?>(
@"select coalesce(EpisodeId, MovieId, MusicVideoId, OtherVideoId, SongId, ImageId, RemoteStreamId) as MediaItemId
from MediaVersion MV
inner join MediaFile MF on MV.Id = MF.MediaVersionId
where MF.Path = @Path",
new { Path = path })
.Map(Optional);
foreach (int mediaItemId in maybeMediaItemId)
{
Option<MediaItem> maybeMediaItem = await dbContext.MediaItems
.AsNoTracking()
.Include(mi => mi.LibraryPath)
.ThenInclude(lp => lp.Library)
.SelectOneAsync(mi => mi.Id, mi => mi.Id == mediaItemId, cancellationToken);
foreach (MediaItem mediaItem in maybeMediaItem)
{
Option<Library> maybeIncomingLibrary = await dbContext.Libraries
.AsNoTracking()
.Filter(l => l.Paths.Any(p => p.Id == libraryPathId))
.SingleOrDefaultAsync(cancellationToken)
.Map(Optional);
foreach (Library incomingLibrary in maybeIncomingLibrary)
{
string incomingLibraryType = incomingLibrary switch
{
PlexLibrary => "Plex Library",
EmbyLibrary => "Emby Library",
JellyfinLibrary => "Jellyfin Library",
_ => "Local Library"
};
string existingLibraryType = mediaItem.LibraryPath.Library switch
{
PlexLibrary => "Plex Library",
EmbyLibrary => "Emby Library",
JellyfinLibrary => "Jellyfin Library",
_ => "Local Library"
};
string libraryName = mediaItem.LibraryPath.Library.Name;
logger.LogDebug(
"Unable to add media item to {IncomingLibraryType} '{IncomingLibraryName}'; {LibraryType} '{LibraryName}' already contains path {Path}",
incomingLibraryType,
incomingLibrary.Name,
existingLibraryType,
libraryName,
path);
return true;
}
}
}
return false;
}
private async Task<List<string>> GetAllLanguageCodes()
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
return await dbContext.Connection.QueryAsync<string>(
@"SELECT LanguageCode FROM
(SELECT Language AS LanguageCode
FROM MediaStream WHERE Language IS NOT NULL
UNION ALL SELECT Language AS LanguageCode
FROM Subtitle WHERE Language IS NOT NULL
UNION ALL SELECT PreferredAudioLanguageCode AS LanguageCode
FROM Channel WHERE PreferredAudioLanguageCode IS NOT NULL) AS A
GROUP BY LanguageCode
ORDER BY COUNT(LanguageCode) DESC")
.Map(result => result.ToList());
}
}