2023-02-11 04:16:34 +03:00
|
|
|
module PublishHelperBot.YoutubeDl
|
|
|
|
|
|
|
|
open System
|
|
|
|
open System.Collections.Generic
|
|
|
|
open System.IO
|
|
|
|
open System.Net.Http
|
|
|
|
open System.Text
|
2023-02-25 00:04:13 +03:00
|
|
|
open System.Threading.Tasks
|
2023-02-11 04:16:34 +03:00
|
|
|
open Microsoft.FSharp.Core
|
|
|
|
open Newtonsoft.Json
|
2023-02-25 00:04:13 +03:00
|
|
|
open PublishHelperBot.Environment
|
2023-02-11 04:16:34 +03:00
|
|
|
open Telegram.Bot
|
|
|
|
open Telegram.Bot.Types.InputFiles
|
|
|
|
|
2023-02-25 00:04:13 +03:00
|
|
|
type ChatId = int64
|
|
|
|
|
|
|
|
type CreateYoutubeDlJob = {
|
|
|
|
url: string
|
|
|
|
savePath: string
|
2023-02-11 04:16:34 +03:00
|
|
|
}
|
2023-02-25 00:04:13 +03:00
|
|
|
|
|
|
|
type CreateYoutubeDlJobSuccess = {
|
|
|
|
task: Guid
|
2023-02-11 04:16:34 +03:00
|
|
|
}
|
|
|
|
|
2023-02-25 00:04:13 +03:00
|
|
|
type YoutubeDlStateResponse = {
|
|
|
|
state: string
|
2023-02-11 04:16:34 +03:00
|
|
|
}
|
|
|
|
|
2023-02-25 00:04:13 +03:00
|
|
|
type YoutubeDlError = {
|
|
|
|
message: string
|
2023-02-11 04:16:34 +03:00
|
|
|
}
|
2023-02-25 00:04:13 +03:00
|
|
|
|
|
|
|
type CreateJobResult = Result<CreateYoutubeDlJobSuccess, YoutubeDlError>
|
|
|
|
|
|
|
|
type CheckJobResult = Result<YoutubeDlStateResponse, YoutubeDlError>
|
|
|
|
|
|
|
|
type CleanJobResult = Result<YoutubeDlStateResponse, YoutubeDlError>
|
|
|
|
|
|
|
|
type IYoutubeDlClient =
|
|
|
|
abstract member CreateJob: CreateYoutubeDlJob -> Async<CreateJobResult>
|
|
|
|
abstract member CheckJob: externalId: Guid -> Async<CheckJobResult>
|
|
|
|
abstract member CleanJob: externalId: Guid -> Async<CleanJobResult>
|
|
|
|
|
|
|
|
type YoutubeDlClientConfig = {
|
|
|
|
BaseUrl: string
|
|
|
|
Client: HttpClient
|
2023-02-11 04:16:34 +03:00
|
|
|
}
|
|
|
|
|
2023-02-25 00:04:13 +03:00
|
|
|
[<RequireQualifiedAccess>]
|
|
|
|
module YoutubeDlClient =
|
|
|
|
|
|
|
|
type private YoutubeDlClientActions = Create | Check | Delete
|
|
|
|
|
|
|
|
type private HttpMethods = GET | POST | DELETE
|
|
|
|
|
|
|
|
type private Msg =
|
|
|
|
| CreateJob of CreateYoutubeDlJob * tcs: TaskCompletionSource<CreateJobResult>
|
|
|
|
| CheckJob of externalId: Guid * tcs: TaskCompletionSource<CheckJobResult>
|
|
|
|
| CleanJob of externalId: Guid * tcs: TaskCompletionSource<CleanJobResult>
|
|
|
|
|
|
|
|
let private getApiPrefix baseUrl = $"{baseUrl}api/"
|
|
|
|
|
|
|
|
let private resolvePath baseUrl (action: YoutubeDlClientActions) =
|
|
|
|
let apiPrefix = getApiPrefix baseUrl
|
|
|
|
match action with
|
|
|
|
| Create -> $"{apiPrefix}download"
|
|
|
|
| Check -> $"{apiPrefix}status"
|
|
|
|
| Delete -> $"{apiPrefix}clear"
|
|
|
|
|
|
|
|
let private doHttp
|
|
|
|
(method: HttpMethods)
|
|
|
|
(content: HttpContent)
|
|
|
|
(client: HttpClient)
|
|
|
|
(url: string): Async<Result<'TRes, YoutubeDlError>> =
|
|
|
|
task {
|
|
|
|
try
|
|
|
|
let! contentText = content.ReadAsStringAsync()
|
|
|
|
Logging.logger.Information("Sending request to youtube api, url = {Url}, content = {Content}", url, contentText)
|
|
|
|
let! res =
|
|
|
|
match method with
|
|
|
|
| POST -> client.PostAsync(url, content)
|
|
|
|
| GET -> client.GetAsync(url)
|
|
|
|
| DELETE -> client.DeleteAsync(url)
|
|
|
|
|
|
|
|
let! content = res.Content.ReadAsStringAsync()
|
|
|
|
Logging.logger.Information("Response from youtube api, response = {Response}", content)
|
|
|
|
return
|
|
|
|
match res.IsSuccessStatusCode with
|
|
|
|
| true ->
|
|
|
|
Logging.logger.Information("Response OK")
|
|
|
|
Ok (JsonConvert.DeserializeObject<'TRes>(content))
|
|
|
|
| false ->
|
|
|
|
Logging.logger.Error("Response network error")
|
|
|
|
Error { message = "Unknown network error" }
|
|
|
|
with ex ->
|
|
|
|
Logging.logger.Error(ex, "Youtube api error")
|
|
|
|
return Error { message = ex.Message }
|
|
|
|
} |> Async.AwaitTask
|
|
|
|
|
|
|
|
let private createInbox(config: YoutubeDlClientConfig) = MailboxProcessor.Start(fun inbox ->
|
|
|
|
let rec loop() =
|
|
|
|
async {
|
|
|
|
match! inbox.Receive() with
|
|
|
|
| CreateJob(args, tcs) ->
|
|
|
|
Logging.logger.Information("Received youtube api create job")
|
|
|
|
async {
|
|
|
|
try
|
|
|
|
let json = args |> JsonConvert.SerializeObject
|
|
|
|
Logging.logger.Information("Sending create job = {Job}", json)
|
|
|
|
use content = new StringContent(json, Encoding.UTF8, "application/json")
|
|
|
|
let! result =
|
|
|
|
(config.Client, resolvePath config.BaseUrl Create)
|
|
|
|
||> doHttp POST content
|
|
|
|
Logging.logger.Information("Received response from youtube api for create job")
|
|
|
|
tcs.SetResult(result)
|
|
|
|
|
|
|
|
with ex ->
|
|
|
|
Logging.logger.Error(ex, "Failed to create youtube api job")
|
|
|
|
tcs.SetResult(Error {
|
|
|
|
message = ex.Message
|
|
|
|
})
|
|
|
|
|
|
|
|
} |> Async.Start
|
|
|
|
|
|
|
|
return! loop ()
|
|
|
|
|
|
|
|
| CheckJob (externalId, tcs) ->
|
|
|
|
Logging.logger.Information("Received youtube api check job, externalId = {id}", externalId)
|
|
|
|
async {
|
|
|
|
try
|
|
|
|
let arg = [KeyValuePair("id", externalId.ToString())]
|
|
|
|
use content = new FormUrlEncodedContent(arg)
|
|
|
|
Logging.logger.Information("Sending youtube api check job")
|
|
|
|
let! query = content.ReadAsStringAsync() |> Async.AwaitTask
|
|
|
|
let! result =
|
|
|
|
(config.Client, $"{resolvePath config.BaseUrl Check}?{query}")
|
|
|
|
||> doHttp GET content
|
|
|
|
Logging.logger.Information("Received response from youtube api for check job")
|
|
|
|
tcs.SetResult(result)
|
|
|
|
|
|
|
|
with ex ->
|
|
|
|
Logging.logger.Error(ex, "Failed to check youtube api job")
|
|
|
|
tcs.SetResult(Error {
|
|
|
|
message = ex.Message
|
|
|
|
})
|
|
|
|
} |> Async.Start
|
|
|
|
|
|
|
|
return! loop ()
|
|
|
|
|
|
|
|
| CleanJob(externalId, tcs) ->
|
|
|
|
Logging.logger.Information("Received youtube api clean job, externalId = {id}", externalId)
|
|
|
|
async {
|
|
|
|
try
|
|
|
|
let arg = [KeyValuePair("id", externalId.ToString())]
|
|
|
|
use content = new FormUrlEncodedContent(arg)
|
|
|
|
Logging.logger.Information("Sending youtube api clean job")
|
|
|
|
let! query = content.ReadAsStringAsync() |> Async.AwaitTask
|
|
|
|
let! result =
|
|
|
|
(config.Client, $"{resolvePath config.BaseUrl Delete}?{query}")
|
|
|
|
||> doHttp DELETE content
|
|
|
|
Logging.logger.Information("Received response from youtube api for clean job")
|
|
|
|
tcs.SetResult(result)
|
|
|
|
|
|
|
|
with ex ->
|
|
|
|
Logging.logger.Error(ex, "Failed to clean youtube api job")
|
|
|
|
tcs.SetResult(Error {
|
|
|
|
message = ex.Message
|
|
|
|
})
|
|
|
|
} |> Async.Start
|
|
|
|
|
|
|
|
return! loop ()
|
|
|
|
}
|
|
|
|
loop ()
|
|
|
|
)
|
|
|
|
|
|
|
|
let createClient (config: YoutubeDlClientConfig) =
|
|
|
|
let inbox = createInbox(config)
|
|
|
|
{ new IYoutubeDlClient with
|
|
|
|
member this.CreateJob(args) =
|
|
|
|
let tcs = TaskCompletionSource<_>()
|
|
|
|
inbox.Post(CreateJob(args, tcs))
|
|
|
|
tcs.Task |> Async.AwaitTask
|
|
|
|
|
|
|
|
member this.CheckJob externalId =
|
|
|
|
let tcs = TaskCompletionSource<_>()
|
|
|
|
inbox.Post(CheckJob(externalId, tcs))
|
|
|
|
tcs.Task |> Async.AwaitTask
|
|
|
|
|
|
|
|
member this.CleanJob externalId =
|
|
|
|
let tcs = TaskCompletionSource<_>()
|
|
|
|
inbox.Post(CleanJob(externalId, tcs))
|
|
|
|
tcs.Task |> Async.AwaitTask }
|
|
|
|
|
|
|
|
type TgServiceConfig = {
|
|
|
|
Client: ITelegramBotClient
|
|
|
|
ChannelId: ChatId
|
|
|
|
AdminChatId: ChatId
|
|
|
|
YoutubeDlClient: IYoutubeDlClient
|
2023-02-11 04:16:34 +03:00
|
|
|
}
|
|
|
|
|
2023-02-25 00:04:13 +03:00
|
|
|
type ITgService =
|
|
|
|
abstract member PostVideo: url: string * savePath: string * externalId: Guid -> unit
|
|
|
|
|
|
|
|
[<RequireQualifiedAccess>]
|
|
|
|
module TgService =
|
|
|
|
type private Msg =
|
|
|
|
| PostVideo of url: string * savePath: string * externalId: Guid
|
|
|
|
|
|
|
|
let private createInbox (config: TgServiceConfig) =
|
|
|
|
MailboxProcessor.Start(fun inbox ->
|
|
|
|
let rec loop () =
|
|
|
|
async {
|
|
|
|
match! inbox.Receive() with
|
|
|
|
| PostVideo (url, savePath, externalId) ->
|
|
|
|
try
|
|
|
|
try
|
|
|
|
Logging.logger.Information("Reading file path = {path}", savePath)
|
|
|
|
use file = File.OpenRead(savePath)
|
|
|
|
if (file.Length / 1024L / 1024L) < 50L then
|
|
|
|
let input = InputOnlineFile(file, Path.GetRandomFileName())
|
|
|
|
let caption = $"Source: {url}"
|
|
|
|
Logging.logger.Information(
|
|
|
|
"Sending video to channel, channelId = {channelId}, caption = {caption}",
|
|
|
|
config.ChannelId,
|
|
|
|
caption)
|
|
|
|
do! config.Client.SendVideoAsync(
|
|
|
|
config.ChannelId,
|
|
|
|
input,
|
|
|
|
caption = caption
|
|
|
|
)
|
|
|
|
|> Async.AwaitTask |> Async.Ignore
|
|
|
|
else
|
|
|
|
do! config.Client.SendTextMessageAsync(config.AdminChatId, $"Да блять, видео вышло больше 50мб: {externalId}") |> Async.AwaitTask |> Async.Ignore
|
|
|
|
with ex ->
|
|
|
|
Logging.logger.Error(ex, "Failed to send video")
|
|
|
|
finally
|
|
|
|
Logging.logger.Information("Deleting file path = {path}", savePath)
|
|
|
|
File.Delete(savePath)
|
|
|
|
|
|
|
|
match! config.YoutubeDlClient.CleanJob(externalId) with
|
|
|
|
| Ok _ -> ()
|
|
|
|
| Error _ -> ()
|
|
|
|
return! loop ()
|
|
|
|
}
|
|
|
|
loop ()
|
|
|
|
)
|
|
|
|
|
|
|
|
let createService config =
|
|
|
|
let inbox = createInbox config
|
|
|
|
{ new ITgService with
|
|
|
|
member this.PostVideo(url, savePath, externalId) =
|
|
|
|
inbox.Post(PostVideo(url, savePath, externalId)) }
|
|
|
|
|
|
|
|
type IYoutubeDlService =
|
|
|
|
abstract member AddJob: url: string -> Async<Guid>
|
|
|
|
|
|
|
|
[<RequireQualifiedAccess>]
|
|
|
|
module YoutubeDlService =
|
|
|
|
type private Msg =
|
|
|
|
| AddJob of url: string * TaskCompletionSource<Guid>
|
|
|
|
| CheckJob
|
|
|
|
|
|
|
|
type private JobState =
|
|
|
|
| Created
|
|
|
|
| Awaiting of Guid
|
|
|
|
|
|
|
|
type private YoutubeDlJob = {
|
|
|
|
InternalId: Guid
|
|
|
|
State: JobState
|
|
|
|
Url: string
|
|
|
|
SavePath: string
|
|
|
|
}
|
|
|
|
|
|
|
|
let private createJob (client: IYoutubeDlClient) (job: YoutubeDlJob) =
|
|
|
|
async {
|
|
|
|
Logging.logger.Information("Sending create job to youtube client, job = {job}", job.InternalId)
|
|
|
|
let! result =
|
|
|
|
client.CreateJob {
|
|
|
|
url = job.Url
|
|
|
|
savePath = job.SavePath
|
|
|
|
}
|
|
|
|
match result with
|
|
|
|
| Ok task ->
|
|
|
|
Logging.logger.Information(
|
|
|
|
"Created job on youtube client = {job}, externalId = {externalId}",
|
|
|
|
job.InternalId,
|
|
|
|
task.task)
|
|
|
|
let updated = {
|
|
|
|
job with
|
|
|
|
State = JobState.Awaiting task.task
|
|
|
|
}
|
|
|
|
return Some updated
|
|
|
|
|
|
|
|
| Error e ->
|
|
|
|
Logging.logger.Error(
|
|
|
|
"Failed to create job on client = {job}, message = {message}",
|
|
|
|
job.InternalId,
|
|
|
|
e.message)
|
|
|
|
return None
|
|
|
|
}
|
|
|
|
|
|
|
|
let private getCurrentJob
|
|
|
|
(jobQueue: Queue<YoutubeDlJob>)
|
|
|
|
(current: YoutubeDlJob option) =
|
|
|
|
match current with
|
|
|
|
| Some current ->
|
|
|
|
Some current
|
|
|
|
|
|
|
|
| None ->
|
|
|
|
match jobQueue.TryDequeue() with
|
|
|
|
| true, job ->
|
|
|
|
Some job
|
|
|
|
| _ ->
|
|
|
|
None
|
|
|
|
|
|
|
|
let private createServiceInbox
|
|
|
|
(youtubeDlClient: IYoutubeDlClient)
|
|
|
|
(tgService: ITgService) =
|
|
|
|
|
|
|
|
MailboxProcessor.Start(fun inbox ->
|
|
|
|
let postCheck() =
|
|
|
|
async {
|
|
|
|
do! Async.Sleep(TimeSpan.FromSeconds 5)
|
|
|
|
inbox.Post(CheckJob)
|
|
|
|
} |> Async.Start
|
|
|
|
|
|
|
|
let rec loop
|
|
|
|
(jobQueue: Queue<YoutubeDlJob>)
|
|
|
|
(current: YoutubeDlJob option) =
|
|
|
|
async {
|
|
|
|
match! inbox.Receive() with
|
|
|
|
| AddJob (url, tcs) ->
|
|
|
|
Logging.logger.Information("Adding new url = {url}", url)
|
|
|
|
let id = Guid.NewGuid()
|
|
|
|
let job = {
|
|
|
|
InternalId = id
|
|
|
|
State = Created
|
|
|
|
Url = url
|
|
|
|
SavePath = $"{Path.GetTempFileName()}.mp4"
|
|
|
|
}
|
|
|
|
tcs.SetResult(id)
|
|
|
|
jobQueue.Enqueue(job)
|
|
|
|
|
|
|
|
if current.IsNone then
|
|
|
|
inbox.Post(CheckJob)
|
|
|
|
|
|
|
|
Logging.logger.Information(
|
|
|
|
"Added new job = {job}, url = {url}, path = {path}",
|
|
|
|
job.InternalId,
|
|
|
|
url,
|
|
|
|
job.SavePath)
|
|
|
|
return! loop jobQueue current
|
|
|
|
|
|
|
|
| CheckJob ->
|
|
|
|
let currentJob = getCurrentJob jobQueue current
|
|
|
|
match currentJob with
|
|
|
|
| Some job ->
|
|
|
|
Logging.logger.Information("Checking job = {job}, state = {state}", job.InternalId, job.State)
|
|
|
|
match job.State with
|
|
|
|
| Created ->
|
|
|
|
match! createJob youtubeDlClient job with
|
|
|
|
| Some job ->
|
|
|
|
postCheck()
|
|
|
|
return! loop jobQueue (Some job)
|
|
|
|
|
|
|
|
| None ->
|
|
|
|
postCheck()
|
|
|
|
return! loop jobQueue None
|
|
|
|
|
|
|
|
| Awaiting externalId ->
|
|
|
|
Logging.logger.Information("Checking job = {job}", externalId)
|
|
|
|
let! task = youtubeDlClient.CheckJob(externalId)
|
|
|
|
match task with
|
|
|
|
| Ok x when x.state.Equals("Finished", StringComparison.OrdinalIgnoreCase) ->
|
|
|
|
Logging.logger.Information("Sending post video from job = {job}", externalId)
|
|
|
|
tgService.PostVideo(job.Url, job.SavePath, externalId)
|
|
|
|
postCheck()
|
|
|
|
return! loop jobQueue None
|
|
|
|
|
|
|
|
| Error e ->
|
|
|
|
Logging.logger.Error(
|
|
|
|
"Failed to receive video from youtube client, job = {job}, message = {message}",
|
|
|
|
externalId,
|
|
|
|
e.message)
|
|
|
|
File.Delete(job.SavePath)
|
|
|
|
return! loop jobQueue None
|
|
|
|
|
|
|
|
| _ ->
|
|
|
|
Logging.logger.Information(
|
|
|
|
"Waiting for job to complete, job = {job}",
|
|
|
|
externalId)
|
|
|
|
postCheck()
|
|
|
|
return! loop jobQueue current
|
|
|
|
|
|
|
|
| None ->
|
|
|
|
return! loop jobQueue None
|
|
|
|
}
|
|
|
|
loop (Queue()) None
|
|
|
|
)
|
|
|
|
|
|
|
|
let createService youtubeDlClient tgService =
|
|
|
|
let inbox = createServiceInbox youtubeDlClient tgService
|
|
|
|
{ new IYoutubeDlService with
|
|
|
|
member this.AddJob(url) =
|
|
|
|
let tcs = TaskCompletionSource<_>()
|
|
|
|
inbox.Post(AddJob(url, tcs))
|
|
|
|
tcs.Task |> Async.AwaitTask }
|