Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Net 8 #10

Merged
merged 2 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
README.md
**/helm-chart
**/.github
.dockerignore
.gitignore
23 changes: 10 additions & 13 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/runtime:6.0-alpine AS base
FROM mcr.microsoft.com/dotnet/runtime:8.0-alpine AS base
WORKDIR /app
RUN apk add --no-cache --virtual build-deps musl-dev gcc g++ python3-dev &&\
apk add --no-cache py3-pip tzdata &&\
Expand All @@ -12,24 +12,21 @@ ENV TZ=Asia/Taipei
# https://github.com/dotnet/runtime/issues/34126#issuecomment-1104981659
ENV DOTNET_SYSTEM_IO_DISABLEFILELOCKING=true

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["YoutubeLiveChatToDiscord.csproj", "."]
RUN dotnet restore "./YoutubeLiveChatToDiscord.csproj"
RUN dotnet restore "YoutubeLiveChatToDiscord.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "YoutubeLiveChatToDiscord.csproj" -c Release -o /app/build
RUN dotnet build "YoutubeLiveChatToDiscord.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
RUN dotnet publish "YoutubeLiveChatToDiscord.csproj" -c Release -o /app/publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "YoutubeLiveChatToDiscord.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .

RUN addgroup -g 1000 docker && \
adduser -u 1000 -G docker -h /home/docker -s /bin/sh -D docker \
&& chown -R 1000:1000 .
USER docker

RUN mkdir -p /app && chown -R app:app /app
USER app
ENTRYPOINT ["dotnet", "YoutubeLiveChatToDiscord.dll"]
8 changes: 8 additions & 0 deletions GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.

using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage("Style", "IDE0046:轉換至條件運算式", Justification = "<暫止>", Scope = "member", Target = "~M:YoutubeLiveChatToDiscord.Helper.GetOriginalImage(System.String)~System.String")]
24 changes: 11 additions & 13 deletions Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ internal static class ApplicationLogging
internal static ILogger CreateLogger(string categoryName) => LoggerFactory.CreateLogger(categoryName);
}

private static readonly ILogger logger = ApplicationLogging.CreateLogger("Helper");
private static readonly ILogger _logger = ApplicationLogging.CreateLogger("Helper");

/// <summary>
/// 尋找yt-dlp程式路徑
Expand All @@ -25,14 +25,14 @@ public static string WhereIsYt_dlp()
{
// https://stackoverflow.com/a/63021455
string file = "yt-dlp";
string[] paths = Environment.GetEnvironmentVariable("PATH")?.Split(';') ?? Array.Empty<string>();
string[] extensions = Environment.GetEnvironmentVariable("PATHEXT")?.Split(';') ?? Array.Empty<string>();
string[] paths = Environment.GetEnvironmentVariable("PATH")?.Split(';') ?? [];
string[] extensions = Environment.GetEnvironmentVariable("PATHEXT")?.Split(';') ?? [];
string YtdlPath = (from p in new[] { Environment.CurrentDirectory }.Concat(paths)
from e in extensions
let path = Path.Combine(p.Trim(), file + e.ToLower())
where File.Exists(path)
select path)?.FirstOrDefault() ?? "/usr/bin/yt-dlp";
logger.LogDebug("Found yt-dlp at {path}", YtdlPath);
_logger.LogDebug("Found yt-dlp at {path}", YtdlPath);
return YtdlPath;
}

Expand All @@ -42,33 +42,31 @@ where File.Exists(path)
/// <param name="arg"></param>
/// <returns></returns>
internal static Task DiscordWebhookClient_Log(LogMessage arg)
{
return Task.Run(() =>
=> Task.Run(() =>
{
switch (arg.Severity)
{
case LogSeverity.Critical:
logger.LogCritical("{message}", arg);
_logger.LogCritical("{message}", arg);
break;
case LogSeverity.Error:
logger.LogError("{message}", arg);
_logger.LogError("{message}", arg);
break;
case LogSeverity.Warning:
logger.LogWarning("{message}", arg);
_logger.LogWarning("{message}", arg);
break;
case LogSeverity.Info:
logger.LogInformation("{message}", arg);
_logger.LogInformation("{message}", arg);
break;
case LogSeverity.Verbose:
logger.LogTrace("{message}", arg);
_logger.LogTrace("{message}", arg);
break;
case LogSeverity.Debug:
default:
logger.LogDebug("{message}", arg);
_logger.LogDebug("{message}", arg);
break;
}
});
}

/// <summary>
/// 處理Youtube的圖片url,取得原始尺寸圖片
Expand Down
107 changes: 52 additions & 55 deletions LiveChatMonitorWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,26 @@ namespace YoutubeLiveChatToDiscord
{
public class LiveChatMonitorWorker : BackgroundService
{
private readonly ILogger<LiveChatMonitorWorker> logger;
private readonly string id;
private readonly DiscordWebhookClient client;
private readonly FileInfo liveChatFileInfo;
private long position = 0;
private readonly LiveChatDownloadService liveChatDownloadService;
private readonly ILogger<LiveChatMonitorWorker> _logger;
private readonly string _id;
private readonly DiscordWebhookClient _client;
private readonly FileInfo _liveChatFileInfo;
private long _position = 0;
private readonly LiveChatDownloadService _liveChatDownloadService;

public LiveChatMonitorWorker(
ILogger<LiveChatMonitorWorker> _logger,
DiscordWebhookClient _client,
LiveChatDownloadService _liveChatDownloadService
ILogger<LiveChatMonitorWorker> logger,
DiscordWebhookClient client,
LiveChatDownloadService liveChatDownloadService
)
{
(logger, client, liveChatDownloadService) = (_logger, _client, _liveChatDownloadService);
client.Log += Helper.DiscordWebhookClient_Log;
(_logger, _client, _liveChatDownloadService) = (logger, client, liveChatDownloadService);
_client.Log += Helper.DiscordWebhookClient_Log;

id = Environment.GetEnvironmentVariable("VIDEO_ID") ?? "";
if (string.IsNullOrEmpty(id)) throw new ArgumentException(nameof(id));
_id = Environment.GetEnvironmentVariable("VIDEO_ID") ?? "";
if (string.IsNullOrEmpty(_id)) throw new ArgumentException(nameof(_id));

liveChatFileInfo = new($"{id}.live_chat.json");
_liveChatFileInfo = new($"{_id}.live_chat.json");
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
Expand All @@ -36,38 +36,35 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (liveChatDownloadService.DownloadProcess.IsCompleted)
if (_liveChatDownloadService.downloadProcess.IsCompleted)
{
_ = liveChatDownloadService.ExecuteAsync(stoppingToken)
.ContinueWith((_) =>
{
logger.LogInformation("yt-dlp is stopped.");
}, stoppingToken);
_ = _liveChatDownloadService.ExecuteAsync(stoppingToken)
.ContinueWith((_) => _logger.LogInformation("yt-dlp is stopped."), stoppingToken);
}

logger.LogInformation("Wait 10 seconds.");
_logger.LogInformation("Wait 10 seconds.");
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
liveChatFileInfo.Refresh();
_liveChatFileInfo.Refresh();

try
{
if (!liveChatFileInfo.Exists)
if (!_liveChatFileInfo.Exists)
{
throw new FileNotFoundException(null, liveChatFileInfo.FullName);
throw new FileNotFoundException(null, _liveChatFileInfo.FullName);
}

await Monitoring(stoppingToken);
}
catch (FileNotFoundException e)
{
logger.LogWarning("Json file not found. {FileName}", e.FileName);
_logger.LogWarning("Json file not found. {FileName}", e.FileName);
}
}
}
catch (TaskCanceledException) { }
finally
{
logger.LogError("Wait 10 seconds before closing the program. This is to prevent a restart loop from hanging the machine.");
_logger.LogError("Wait 10 seconds before closing the program. This is to prevent a restart loop from hanging the machine.");
#pragma warning disable CA2016 // 將 'CancellationToken' 參數轉送給方法
await Task.Delay(TimeSpan.FromSeconds(10));
#pragma warning restore CA2016 // 將 'CancellationToken' 參數轉送給方法
Expand All @@ -87,32 +84,32 @@ private async Task Monitoring(CancellationToken stoppingToken)
#if !DEBUG
if (null == Environment.GetEnvironmentVariable("SKIP_STARTUP_WAITING"))
{
logger.LogInformation("Wait 1 miunute to skip old chats");
_logger.LogInformation("Wait 1 miunute to skip old chats");
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
liveChatFileInfo.Refresh();
_liveChatFileInfo.Refresh();
}
#endif

position = liveChatFileInfo.Length;
logger.LogInformation("Start at position: {position}", position);
logger.LogInformation("Start Monitoring!");
_position = _liveChatFileInfo.Length;
_logger.LogInformation("Start at position: {position}", _position);
_logger.LogInformation("Start Monitoring!");

while (!stoppingToken.IsCancellationRequested)
{
liveChatFileInfo.Refresh();
if (liveChatFileInfo.Length > position)
_liveChatFileInfo.Refresh();
if (_liveChatFileInfo.Length > _position)
{
await ProcessChats(stoppingToken);
}
else if (liveChatDownloadService.DownloadProcess.IsCompleted)
else if (_liveChatDownloadService.downloadProcess.IsCompleted)
{
logger.LogInformation("Download process is stopped. Restart monitoring.");
_logger.LogInformation("Download process is stopped. Restart monitoring.");
return;
}
else
{
position = liveChatFileInfo.Length;
logger.LogTrace("No new chat. Wait 10 seconds.");
_position = _liveChatFileInfo.Length;
_logger.LogTrace("No new chat. Wait 10 seconds.");
// 每10秒檢查一次json檔
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
Expand All @@ -127,14 +124,14 @@ private async Task Monitoring(CancellationToken stoppingToken)
/// <exception cref="FileNotFoundException"></exception>
private async Task GetVideoInfo(CancellationToken stoppingToken)
{
FileInfo videoInfo = new($"{id}.info.json");
FileInfo videoInfo = new($"{_id}.info.json");
if (!videoInfo.Exists)
{
// Chat json file 在 VideoInfo json file之後被產生,理論上這段不會進來
throw new FileNotFoundException(null, videoInfo.FullName);
}

Info? info = JsonConvert.DeserializeObject<Info>(await new StreamReader(videoInfo.OpenRead()).ReadToEndAsync());
Info? info = JsonConvert.DeserializeObject<Info>(await new StreamReader(videoInfo.OpenRead()).ReadToEndAsync(stoppingToken));
string? Title = info?.title;
string? ChannelId = info?.channel_id;
string? thumb = info?.thumbnail;
Expand All @@ -155,17 +152,17 @@ private async Task ProcessChats(CancellationToken stoppingToken)
// 這是.NET Core在Linux、Windows上關於鎖定設計的描述: https://github.com/dotnet/runtime/pull/55256
// 如果要繞過這個問題,從.NET 6開始,可以加上環境變數「DOTNET_SYSTEM_IO_DISABLEFILELOCKING」讓FileStream「不」遵守鎖定。
// (本專案已在Dockerfile加上此環境變數)
using FileStream fs = new(liveChatFileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using FileStream fs = new(_liveChatFileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using StreamReader sr = new(fs);

sr.BaseStream.Seek(position, SeekOrigin.Begin);
while (position < sr.BaseStream.Length)
sr.BaseStream.Seek(_position, SeekOrigin.Begin);
while (_position < sr.BaseStream.Length)
{
string? str = "";
try
{
str = await sr.ReadLineAsync();
position = sr.BaseStream.Position;
str = await sr.ReadLineAsync(stoppingToken);
_position = sr.BaseStream.Position;
if (string.IsNullOrEmpty(str)) continue;

Chat? chat = JsonConvert.DeserializeObject<Chat>(str);
Expand All @@ -175,17 +172,17 @@ private async Task ProcessChats(CancellationToken stoppingToken)
}
catch (JsonSerializationException e)
{
logger.LogError("{error}", e.Message);
logger.LogError("{originalString}", str);
_logger.LogError("{error}", e.Message);
_logger.LogError("{originalString}", str);
}
catch (ArgumentException e)
{
logger.LogError("{error}", e.Message);
logger.LogError("{originalString}", str);
_logger.LogError("{error}", e.Message);
_logger.LogError("{originalString}", str);
}
catch (IOException e)
{
logger.LogError("{error}", e.Message);
_logger.LogError("{error}", e.Message);
break;
}
}
Expand All @@ -202,7 +199,7 @@ private async Task BuildRequestAndSendToDiscord(Chat chat, CancellationToken sto
{
EmbedBuilder eb = new();
eb.WithTitle(Environment.GetEnvironmentVariable("TITLE") ?? "")
.WithUrl($"https://youtu.be/{id}")
.WithUrl($"https://youtu.be/{_id}")
.WithThumbnailUrl(Helper.GetOriginalImage(Environment.GetEnvironmentVariable("VIDEO_THUMB")));
string author = "";

Expand Down Expand Up @@ -397,13 +394,13 @@ private async Task BuildRequestAndSendToDiscord(Chat chat, CancellationToken sto
) { return; }
else
{
logger.LogWarning("Message type not supported, skip sending to discord.");
_logger.LogWarning("Message type not supported, skip sending to discord.");
throw new ArgumentException("Message type not supported", nameof(chat));
}

if (stoppingToken.IsCancellationRequested) return;

logger.LogDebug("Sending Request to Discord: {author}: {message}", author, eb.Description);
_logger.LogDebug("Sending Request to Discord: {author}: {message}", author, eb.Description);

try
{
Expand All @@ -420,15 +417,15 @@ private async Task BuildRequestAndSendToDiscord(Chat chat, CancellationToken sto

// The rate for Discord webhooks are 30 requests/minute per channel.
// Be careful when you run multiple instances in the same channel!
logger.LogTrace("Wait 2 seconds for discord webhook rate limit");
_logger.LogTrace("Wait 2 seconds for discord webhook rate limit");
await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);

Task SendMessage()
=> client.SendMessageAsync(embeds: new Embed[] { eb.Build() })
=> _client.SendMessageAsync(embeds: new Embed[] { eb.Build() })
.ContinueWith(async p =>
{
ulong messageId = await p;
logger.LogDebug("Message sent to discord, message id: {messageId}", messageId);
_logger.LogDebug("Message sent to discord, message id: {messageId}", messageId);
}, stoppingToken);
}
}
Expand Down
3 changes: 1 addition & 2 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
{
services.AddHostedService<LiveChatMonitorWorker>()
.AddSingleton<LiveChatDownloadService>()
.AddSingleton<DiscordWebhookClient>((service) =>
new DiscordWebhookClient(Environment.GetEnvironmentVariable("WEBHOOK")));
.AddSingleton((service) => new DiscordWebhookClient(Environment.GetEnvironmentVariable("WEBHOOK")));
})
.Build();

Expand Down
Loading