publishhelperbot/PublishHelperBot/YoutubeDl.fs

177 lines
6.0 KiB
Forth
Raw Normal View History

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
open System.Threading
open Microsoft.FSharp.Core
open Newtonsoft.Json
open Nito.AsyncEx
open Telegram.Bot
open Telegram.Bot.Types.InputFiles
type public CreateYoutubeDLUrl = string
type public ChatId = int64
type public CreateYoutubeDLJob = {
url: string
savePath: string
}
type public CreateYoutubeDLJobSuccess = {
task: Guid
}
type public YoutubeDlStateResponse = {
state: string
}
type public YoutubeDlError = {
message: string
}
type public YoutubeDlJob<'A> = {
internalId: Guid
externalId: 'A
url: string
state: string
savePath: string
}
type YoutubeDlClientActions = Create | Check | Delete
type HttpMethods = GET | POST | DELETE
type CreateJobResult = Result<CreateYoutubeDLJobSuccess, YoutubeDlError> Async
type CheckJobResult = Result<YoutubeDlStateResponse, YoutubeDlError> Async
type CleanJobResult = Result<YoutubeDlStateResponse, YoutubeDlError> Async
type StartYoutubeDlServiceArgs = HttpClient * CreateYoutubeDLUrl * ITelegramBotClient * ChatId * CancellationToken
type YoutubeDlJobWithId = YoutubeDlJob<Guid>
type YoutubeDlJobWithoutId = YoutubeDlJob<unit>
type YoutubeDlCurrentJob =
| Created of YoutubeDlJobWithoutId
| Awaiting of YoutubeDlJobWithId
| Downloaded of YoutubeDlJobWithId
| Done of YoutubeDlJobWithId
| None of unit
let inline (<!>) (lck: AsyncLock) f = async {
use! __ = Async.AwaitTask <| lck.LockAsync().AsTask()
return! f
}
type YoutubeDlClient(baseUrl: string, client: HttpClient) =
let lock = AsyncLock()
let apiPrefix = $"{baseUrl}api/";
let ResolvePath(action: YoutubeDlClientActions) =
match action with
| Create -> $"{apiPrefix}download"
| Check -> $"{apiPrefix}status"
| Delete -> $"{apiPrefix}clear"
let doHttp (url: string, method: HttpMethods, content: HttpContent): Result<'TRes, YoutubeDlError> Async = async {
try
let! res =
match method with
| POST -> client.PostAsync(url, content) |> Async.AwaitTask
| GET -> client.GetAsync(url) |> Async.AwaitTask
| DELETE -> client.DeleteAsync(url) |> Async.AwaitTask
let! content = res.Content.ReadAsStringAsync() |> Async.AwaitTask
return
match res.IsSuccessStatusCode with
| true -> Ok (JsonConvert.DeserializeObject<'TRes> <| content)
| false -> Error { message = "Unknown network error" }
with
| ex -> return Error { message = ex.Message }
}
member this.CreateJob(model: CreateYoutubeDLJob): CreateJobResult = lock <!> async {
use content = new StringContent(JsonConvert.SerializeObject <| model, Encoding.UTF8, "application/json")
return! doHttp <| (ResolvePath Create, POST, content)
}
member this.CheckJob(id: Guid): CheckJobResult = lock <!> async {
let arg = [KeyValuePair("id", id.ToString())]
use content = new FormUrlEncodedContent(arg)
let! query = content.ReadAsStringAsync() |> Async.AwaitTask
return! doHttp <| ($"{ResolvePath Check}?{query}", GET, content)
}
member this.CleanJob(id: Guid): CleanJobResult = lock <!> async {
let arg = [KeyValuePair("id", id.ToString())]
use content = new FormUrlEncodedContent(arg)
let! query = content.ReadAsStringAsync() |> Async.AwaitTask
return! doHttp <| ($"{ResolvePath Delete}?{query}", DELETE, content)
}
type YoutubeDlBackgroundService(requirements: StartYoutubeDlServiceArgs) =
let (http, url, tg, chatId, ct) = requirements
let lock = AsyncLock()
let mutable currentJob: YoutubeDlCurrentJob = None ()
let jobPool = Queue<YoutubeDlJobWithoutId>()
let ytClient = YoutubeDlClient <| (url, http)
let mapJobToApi (job: YoutubeDlJob<_>): CreateYoutubeDLJob = {
url = job.url
savePath = job.savePath
}
let attachExternalId (id: Guid, job: YoutubeDlJobWithoutId): YoutubeDlCurrentJob =
2023-02-11 05:28:01 +03:00
Awaiting { internalId = job.internalId; state = job.state; url = job.url; externalId = id; savePath = job.savePath }
2023-02-11 04:16:34 +03:00
let tryAssignNewJob() = async {
let (result, job) = jobPool.TryDequeue()
match result with
| true -> currentJob <- Created job
| false -> currentJob <- None ()
}
let uploadToYtDl(job: YoutubeDlJobWithoutId) = async {
match! ytClient.CreateJob <| mapJobToApi job with
| Ok x -> currentJob <- attachExternalId <| (x.task, job)
// TODO: Logging!
| Error _ -> currentJob <- None ()
}
let checkJob(job: YoutubeDlJobWithId) = async {
match! ytClient.CheckJob <| job.externalId with
| Ok x when x.state.Equals("Finished", StringComparison.OrdinalIgnoreCase) -> currentJob <- Downloaded job
| Error _ -> currentJob <- None ()
| _ -> ()
// That's take a while
do! Async.Sleep 5000
}
let postVideo(job: YoutubeDlJobWithId) = async {
use file = File.OpenRead <| job.savePath
let input = InputOnlineFile(file, job.savePath)
let caption = $"Source: {job.url}"
do! tg.SendVideoAsync(chatId, input, caption = caption) |> Async.AwaitTask |> Async.Ignore
currentJob <- Done job
}
let cleanUp(job: YoutubeDlJobWithId) = async {
2023-02-11 05:28:01 +03:00
File.Delete <| job.savePath
2023-02-11 04:16:34 +03:00
match! ytClient.CleanJob <| job.externalId with
| Ok _ -> currentJob <- None ()
| Error _ -> currentJob <- None ()
}
let chooseAction() = lock <!> async {
do! match currentJob with
| None _ -> tryAssignNewJob()
| Created x -> x |> uploadToYtDl
| Awaiting x -> x |> checkJob
| Downloaded x -> x |> postVideo
| Done x -> x |> cleanUp
}
let rec loop () = async {
do! match ct.IsCancellationRequested with
| false -> chooseAction()
| true -> async { () }
do! Async.Sleep 150
return! loop()
}
member public this.StartYoutubeDlService() = loop() |> Async.Start
member public this.EnqueueJob(url: string) = lock <!> async {
let id = Guid.NewGuid()
2023-02-11 05:28:01 +03:00
jobPool.Enqueue({ internalId = id; externalId = (); state = "new"; url = url; savePath = $"{Path.GetTempFileName()}.mp4" })
2023-02-11 04:16:34 +03:00
return id
}