Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
jim60105 committed May 24, 2024
2 parents c491158 + 8ded74d commit 7f4eebc
Show file tree
Hide file tree
Showing 17 changed files with 146 additions and 89 deletions.
4 changes: 2 additions & 2 deletions Helper/YoutubeDLHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ from e in extensions
where File.Exists(path)
select path)?.FirstOrDefault();

Log.Debug("Found yt-dlp.exe at {YtdlpPath}", _YtdlpPath);
Log.Debug("Found ffmpeg.exe at {FFmpegPath}", _FFmpegPath);
Log.Debug("Found yt-dlp at {YtdlpPath}", _YtdlpPath);
Log.Debug("Found ffmpeg at {FFmpegPath}", _FFmpegPath);

return (_YtdlpPath, _FFmpegPath);
}
Expand Down
2 changes: 2 additions & 0 deletions Interfaces/IJobService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ namespace LivestreamRecorderService.Interfaces;

public interface IJobService
{
Task<bool> IsJobMissing(Video video, CancellationToken cancellation);
Task<bool> IsJobMissing(string keyword, CancellationToken cancellation);
Task<bool> IsJobFailedAsync(Video video, CancellationToken cancellation = default);
Task<bool> IsJobFailedAsync(string keyword, CancellationToken cancellation = default);
Task<bool> IsJobSucceededAsync(Video video, CancellationToken cancellation = default);
Expand Down
3 changes: 3 additions & 0 deletions LivestreamRecorderService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<EnableTrimAnalyzer>false</EnableTrimAnalyzer>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>.</DockerfileContext>
<ContainerDevelopmentMode>Fast</ContainerDevelopmentMode>
<DockerfileFastModeStage>debug</DockerfileFastModeStage>
<Configurations>CouchDB;CosmosDB;AzureCosmosDB_Release;ApacheCouchDB_Release</Configurations>
</PropertyGroup>
Expand All @@ -23,6 +24,8 @@
<DefineConstants>$(DefineConstants);COUCHDB;RELEASE</DefineConstants>
<Optimize>True</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='CouchDB|AnyCPU'" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='CosmosDB|AnyCPU'" />
<ItemGroup>
<Compile Remove="LivestreamRecorder.DB\**" />
<Content Remove="LivestreamRecorder.DB\**" />
Expand Down
6 changes: 0 additions & 6 deletions LivestreamRecorderService.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LivestreamRecorderService",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LivestreamRecorder.DB", "LivestreamRecorder.DB\LivestreamRecorder.DB.csproj", "{194EA7A4-7735-40ED-B89A-AFAC094B91C0}"
EndProject
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{D9705463-12BD-4589-B8E0-53C6EE792EE8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "方案項目", "方案項目", "{05516183-EF2E-4C8F-B2F3-97C2222C1723}"
ProjectSection(SolutionItems) = preProject
.github\workflows\docker_publish.yml = .github\workflows\docker_publish.yml
Expand Down Expand Up @@ -38,10 +36,6 @@ Global
{194EA7A4-7735-40ED-B89A-AFAC094B91C0}.CosmosDB|Any CPU.Build.0 = CosmosDB|Any CPU
{194EA7A4-7735-40ED-B89A-AFAC094B91C0}.CouchDB|Any CPU.ActiveCfg = CouchDB|Any CPU
{194EA7A4-7735-40ED-B89A-AFAC094B91C0}.CouchDB|Any CPU.Build.0 = CouchDB|Any CPU
{D9705463-12BD-4589-B8E0-53C6EE792EE8}.ApacheCouchDB_Release|Any CPU.ActiveCfg = Release|Any CPU
{D9705463-12BD-4589-B8E0-53C6EE792EE8}.AzureCosmosDB_Release|Any CPU.ActiveCfg = Release|Any CPU
{D9705463-12BD-4589-B8E0-53C6EE792EE8}.CosmosDB|Any CPU.ActiveCfg = Release|Any CPU
{D9705463-12BD-4589-B8E0-53C6EE792EE8}.CouchDB|Any CPU.ActiveCfg = CouchDB|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
1 change: 1 addition & 0 deletions LivestreamRecorderService.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=libaom/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=sharedvolume/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=telop/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=twitcasting/@EntryIndexedValue">True</s:Boolean>
Expand Down
9 changes: 8 additions & 1 deletion ScopedServices/PlatformService/FC2Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ public override async Task UpdateVideosDataAsync(Channel channel, CancellationTo
case VideoStatus.Skipped:
logger.LogTrace("{videoId} is rejected for recording.", video.id);
return;
case VideoStatus.Missing:
logger.LogWarning(
"{videoId} has been marked missing. It is possible that a server malfunction occurred during the previous recording. Changed its state back to Recording.",
video.id);

video.Status = VideoStatus.WaitingToRecord;
break;
case VideoStatus.Archived:
case VideoStatus.PermanentArchived:
logger.LogWarning(
Expand All @@ -98,8 +105,8 @@ public override async Task UpdateVideosDataAsync(Channel channel, CancellationTo
case VideoStatus.Pending:
case VideoStatus.WaitingToDownload:
case VideoStatus.Downloading:
//case VideoStatus.Uploading:
case VideoStatus.Expired:
case VideoStatus.Missing:
case VideoStatus.Error:
case VideoStatus.Exist:
case VideoStatus.Edited:
Expand Down
31 changes: 16 additions & 15 deletions ScopedServices/PlatformService/PlatformService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public bool StepInterval(int elapsedTime)
$"Failed to fetch video data from yt-dlp for URL: {url}. Errors: {string.Join(' ', res.ErrorOutput)}");
}

var videoData = res.Data;
YtdlpVideoData? videoData = res.Data;
return videoData;
}
catch (Exception e)
Expand Down Expand Up @@ -190,21 +190,21 @@ public bool StepInterval(int elapsedTime)
throw new ArgumentNullException(nameof(path));
}

string? extension, contentType, pathInStorage, tempPath;
string? contentType, pathInStorage, tempPath;
try
{
using var client = HttpClientFactory.CreateClient();
var response = await client.GetAsync(url, cancellation);
using HttpClient client = HttpClientFactory.CreateClient();
HttpResponseMessage response = await client.GetAsync(url, cancellation);
response.EnsureSuccessStatusCode();

contentType = response.Content.Headers.ContentType?.MediaType;
extension = MimeUtility.GetExtensions(contentType)?.FirstOrDefault();
string? extension = MimeUtility.GetExtensions(contentType)?.FirstOrDefault();
extension = extension == "jpeg" ? "jpg" : extension;
pathInStorage = $"{path}.{extension}";

tempPath = Path.GetTempFileName();
tempPath = Path.ChangeExtension(tempPath, extension);
await using var contentStream = await response.Content.ReadAsStreamAsync(cancellation);
await using Stream contentStream = await response.Content.ReadAsStreamAsync(cancellation);
await using var fileStream = new FileStream(tempPath, FileMode.Create);
await contentStream.CopyToAsync(fileStream, cancellation);
}
Expand All @@ -216,16 +216,16 @@ public bool StepInterval(int elapsedTime)

try
{
List<Task> tasks =
[
StorageService.UploadPublicFileAsync(contentType, pathInStorage, tempPath, cancellation),
StorageService.UploadPublicFileAsync(KnownMimeTypes.Avif,
$"{path}.avif",
await ImageHelper.ConvertToAvifAsync(tempPath),
cancellation)
];
await StorageService.UploadPublicFileAsync(contentType: contentType,
pathInStorage: pathInStorage,
filePathToUpload: tempPath,
cancellation: cancellation);

await StorageService.UploadPublicFileAsync(contentType: KnownMimeTypes.Avif,
pathInStorage: $"{path}.avif",
filePathToUpload: await ImageHelper.ConvertToAvifAsync(tempPath),
cancellation: cancellation);

await Task.WhenAll(tasks);
return pathInStorage;
}
catch (Exception e)
Expand All @@ -242,6 +242,7 @@ await ImageHelper.ConvertToAvifAsync(tempPath),
}
catch (IOException)
{
// ignored
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions ScopedServices/PlatformService/TwitcastingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ public override async Task UpdateVideosDataAsync(Channel channel, CancellationTo
case VideoStatus.Skipped:
logger.LogTrace("{videoId} is rejected for recording.", video.id);
return;
case VideoStatus.Missing:
logger.LogWarning(
"{videoId} has been marked missing. It is possible that a server malfunction occurred during the previous recording. Changed its state back to Recording.",
video.id);

video.Status = VideoStatus.WaitingToRecord;
break;
case VideoStatus.Archived:
case VideoStatus.PermanentArchived:
logger.LogWarning(
Expand All @@ -79,7 +86,20 @@ public override async Task UpdateVideosDataAsync(Channel channel, CancellationTo

video.Status = VideoStatus.WaitingToRecord;
break;

case VideoStatus.Unknown:
case VideoStatus.Scheduled:
case VideoStatus.Pending:
case VideoStatus.WaitingToDownload:
case VideoStatus.Downloading:
//case VideoStatus.Uploading:
case VideoStatus.Expired:
case VideoStatus.Error:
case VideoStatus.Exist:
case VideoStatus.Edited:
case VideoStatus.Deleted:
default:
// All cases should be handled
logger.LogWarning("{videoId} is in {status}.", video.id, Enum.GetName(typeof(VideoStatus), video.Status));
return;
}
Expand Down
27 changes: 27 additions & 0 deletions ScopedServices/PlatformService/TwitchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,33 @@ public override async Task UpdateVideosDataAsync(Channel channel, CancellationTo
case VideoStatus.Skipped:
logger.LogTrace("{videoId} is rejected for recording.", video.id);
return;
case VideoStatus.Missing:
logger.LogWarning(
"{videoId} has been marked missing. It is possible that a server malfunction occurred during the previous recording. Changed its state back to Recording.",
video.id);

video.Status = VideoStatus.WaitingToRecord;
break;
case VideoStatus.Archived:
case VideoStatus.PermanentArchived:
logger.LogWarning(
"{videoId} has already been archived. It is possible that an internet disconnect occurred during the process. Changed its state back to Recording.",
video.id);

video.Status = VideoStatus.WaitingToRecord;
break;

case VideoStatus.Unknown:
case VideoStatus.Scheduled:
case VideoStatus.Pending:
case VideoStatus.WaitingToDownload:
case VideoStatus.Downloading:
//case VideoStatus.Uploading:
case VideoStatus.Expired:
case VideoStatus.Error:
case VideoStatus.Exist:
case VideoStatus.Edited:
case VideoStatus.Deleted:
default:
logger.LogWarning("{videoId} is in {status}, skip.", video.id, Enum.GetName(typeof(VideoStatus), video.Status));
return;
Expand Down
27 changes: 22 additions & 5 deletions SingletonServices/ACIService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ public class AciService(ILogger<AciService> logger,
private const string FallbackRegistry = "recordermoe/";
private readonly string _resourceGroupName = options.Value.ContainerInstance!.ResourceGroupName;

public Task<bool> IsJobMissing(Video video, CancellationToken cancellation)
{
return IsJobMissing(NameHelper.CleanUpInstanceName(video.id), cancellation);
}

public async Task<bool> IsJobMissing(string keyword, CancellationToken cancellation)
{
return null == (await GetResourceByKeywordAsync(keyword, cancellation));
}

public Task<bool> IsJobSucceededAsync(Video video, CancellationToken cancellation = default)
{
return IsJobSucceededAsync(NameHelper.CleanUpInstanceName(video.id), cancellation);
Expand All @@ -29,7 +39,9 @@ public Task<bool> IsJobSucceededAsync(Video video, CancellationToken cancellatio
public async Task<bool> IsJobSucceededAsync(string keyword, CancellationToken cancellation = default)
{
ContainerGroupResource? resource = await GetResourceByKeywordAsync(keyword, cancellation);
return null != resource && resource.HasData && resource.Data.InstanceView.State == "Succeeded";
return null != resource
&& resource.HasData
&& resource.Data.InstanceView.State == "Succeeded";
}

public Task<bool> IsJobFailedAsync(Video video, CancellationToken cancellation = default)
Expand All @@ -40,7 +52,9 @@ public Task<bool> IsJobFailedAsync(Video video, CancellationToken cancellation =
public async Task<bool> IsJobFailedAsync(string keyword, CancellationToken cancellation)
{
ContainerGroupResource? resource = await GetResourceByKeywordAsync(keyword, cancellation);
return null == resource || !resource.HasData || resource.Data.InstanceView.State == "Failed";
return null != resource
&& (!resource.HasData
|| resource.Data.InstanceView.State == "Failed");
}

/// <summary>
Expand Down Expand Up @@ -166,9 +180,12 @@ await armDeploymentCollection.CreateOrUpdateAsync(
resourceGroupResource.GetContainerGroups()
.FirstOrDefault(p => p.Id.Name.Contains(NameHelper.CleanUpInstanceName(keyword)));

return null == containerGroupResourceTemp
? null
: (await resourceGroupResource.GetContainerGroupAsync(containerGroupResourceTemp.Id.Name, cancellation)).Value;
if (null == containerGroupResourceTemp) return null;

Response<ContainerGroupResource> response =
(await resourceGroupResource.GetContainerGroupAsync(containerGroupResourceTemp.Id.Name, cancellation));

return response.HasValue ? response.Value : null;
}

private async Task<ResourceGroupResource> GetResourceGroupAsync(CancellationToken cancellation = default)
Expand Down
10 changes: 10 additions & 0 deletions SingletonServices/KubernetesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ public class KubernetesService(

private readonly string _kubernetesNamespace = options.Value.Namespace ?? "recordermoe";

public Task<bool> IsJobMissing(Video video, CancellationToken cancellation)
{
return IsJobMissing(NameHelper.CleanUpInstanceName(video.id), cancellation);
}

public async Task<bool> IsJobMissing(string keyword, CancellationToken cancellation)
{
return (await GetJobsByKeywordAsync(keyword, cancellation)).Count == 0;
}

public Task<bool> IsJobSucceededAsync(Video video, CancellationToken cancellation = default)
{
return IsJobSucceededAsync(NameHelper.CleanUpInstanceName(video.id), cancellation);
Expand Down
17 changes: 16 additions & 1 deletion SingletonServices/RecordService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,22 @@ public async Task HandledFailedJobsAsync(VideoService videoService, Cancellation
_logger.LogTrace("No videos recording/downloading");

foreach (Video video in videos)
if (await _jobService.IsJobFailedAsync(video, stoppingToken))
if (await _jobService.IsJobMissing(video, stoppingToken))
switch (video.Source)
{
case "Youtube":
await videoService.UpdateVideoStatusAsync(video, VideoStatus.Pending);
_logger.LogWarning("{videoId} is missing. Set status to {status}", video.id, video.Status);
break;
default:
await videoService.UpdateVideoStatusAsync(video, VideoStatus.Missing);
await videoService.UpdateVideoNoteAsync(video,
"This video archive is missing. If you would like to provide it, please contact admin.");

_logger.LogWarning("{videoId} is missing.", video.id);
break;
}
else if (await _jobService.IsJobFailedAsync(video, stoppingToken))
switch (video.Source)
{
case "Youtube":
Expand Down
26 changes: 19 additions & 7 deletions SingletonServices/S3Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Minio;
using Minio.DataModel;
using Minio.DataModel.Args;
using Minio.DataModel.Response;
using Minio.Exceptions;

namespace LivestreamRecorderService.SingletonServices;
Expand Down Expand Up @@ -71,14 +72,25 @@ public async Task UploadPublicFileAsync(string? contentType, string pathInStorag
{
try
{
await minioClient.PutObjectAsync(new PutObjectArgs()
.WithBucket(_options.BucketName_Public)
.WithObject(pathInStorage)
.WithFileName(tempPath)
.WithContentType(contentType),
cancellation);
string bucketNamePublic = _options.BucketName_Public;
PutObjectArgs putObjectArgs = new PutObjectArgs().WithBucket(bucketNamePublic)
.WithObject(pathInStorage)
.WithFileName(tempPath)
.WithContentType(contentType);

PutObjectResponse result = await minioClient.PutObjectAsync(putObjectArgs, cancellation);

logger.LogInformation("Uploaded to S3 {S3Server} {bucket}/{filePath}, {size}, {etag}",
minioClient.Config.Endpoint,
bucketNamePublic,
result.ObjectName,
result.Size,
result.Etag);

if (string.IsNullOrEmpty(result.Etag))
logger.LogWarning("The Etag is empty for the uploaded file at {filePath}.", pathInStorage);
}
catch (MinioException e)
catch (Exception e)
{
logger.LogError(e, "Failed to upload public file: {filePath}", pathInStorage);
}
Expand Down
19 changes: 0 additions & 19 deletions docker-compose.dcproj

This file was deleted.

9 changes: 0 additions & 9 deletions docker-compose.override.yml

This file was deleted.

Loading

0 comments on commit 7f4eebc

Please sign in to comment.