a whole new method - work order mementos

This commit is contained in:
Adam R Grey 2023-04-07 22:24:31 -04:00
parent 9628a7b8b9
commit 826b181cf1
13 changed files with 567 additions and 190 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
tmp/
working/
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.

View File

@ -9,6 +9,7 @@ namespace ttrss_co_client
public string Password { get; set; }
public string PodcastTitlePrefix { get; set; }
public string OnDoneCopy { get; set; }
public string WorkingDirectory { get; set; } = "./working/";
public IEnumerable<FeedAction> feedActions { get; set; }
public class FeedAction
{

View File

@ -2,149 +2,180 @@
using System.Linq;
using System.Diagnostics;
using System.Text.RegularExpressions;
using ttrss_co_client.tasks;
namespace ttrss_co_client
{
class Program
{
private static ttrss.ApiClient ttrssClient;
static async Task Main(string[] args)
{
// var tasktypes = AppDomain.CurrentDomain.GetAssemblies()
// .SelectMany(domainAssembly => domainAssembly.GetTypes())
// .Where(type => type.IsSubclassOf(typeof(Phase1Task)) && !type.IsAbstract)
// .ToList();
// foreach (var subtype in subtypes)
// {
// var inst = (CoClientTask) Activator.CreateInstance(subtype);
// }
var conf = Configure();
Console.WriteLine($"{DateTime.Now.ToLongTimeString()}");
var ttrssClient = new ttrss.ApiClient(conf.BaseURI);
ttrssClient = new ttrss.ApiClient(conf.BaseURI);
await ttrssClient.Login(conf.Username, conf.Password);
var loggedin = await ttrssClient.IsLoggedIn();
Console.WriteLine($"logged in: {loggedin}");
#region phase 1
Console.WriteLine("===== phase 1 =====");
var Phase1Tasks = new List<Task<WorkOrder>>();
Phase1Task.TtrssClient = ttrssClient;
Phase1Task.Conf = conf;
var phase1TaskTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(domainAssembly => domainAssembly.GetTypes())
.Where(type => type.IsSubclassOf(typeof(Phase1Task)) && !type.IsAbstract)
.ToList();
var phase1TaskConcretions = new List<Phase1Task>();
foreach (var phase1TaskType in phase1TaskTypes)
{
var concretion = (Phase1Task) Activator.CreateInstance(phase1TaskType);
concretion.TriggerLabel = conf.feedActions.FirstOrDefault(fa => fa.command == concretion.TaskName).triggerlabelCaption;
phase1TaskConcretions.Add(concretion);
}
Console.WriteLine($"{phase1TaskConcretions.Count()} phase 1 task types understood");
var unreadFeeds = await ttrssClient.GetFeeds(cat_id: -3, unread_only: true);
Console.WriteLine($"{unreadFeeds.Count()} feeds unread");
foreach (var uf in unreadFeeds)
{
Console.WriteLine($"unread feed: {uf.title}");
var headlines = await ttrssClient.GetHeadlines(uf.id, view_mode: ttrss.ApiClient.VIEWMODE.Unread, include_attachments: true);
foreach (var hl in headlines)
{
var labelsWRTFeed = (await ttrssClient.GetLabels(hl.id));
var labelsWRTArticle = (await ttrssClient.GetLabels(hl.id));
var actionsForFeed = conf.feedActions.Where(fa =>
labelsWRTFeed.Where(l => l.@checked).Select(l => l.caption).Contains(fa.triggerlabelCaption))?.ToList();
labelsWRTArticle.Where(l => l.@checked).Select(l => l.caption).Contains(fa.triggerlabelCaption))?.ToList();
if (actionsForFeed != null && actionsForFeed.Any())
{
var sponsorCheck = await SponsorCheck(hl);
if (!sponsorCheck.Item1)
var action = actionsForFeed.First();
Console.WriteLine($" headline {hl.title} has label {action.triggerlabelCaption} / action {action.command}");
var appropriatePhase1Task = phase1TaskConcretions.FirstOrDefault(tt => tt.TaskName == action.command);
if(appropriatePhase1Task != null)
{
await ttrssClient.UpdateArticleNote($"{hl.note}\n[{DateTime.Now.ToLongTimeString()}] sponsorcheck: {sponsorCheck.Item2}", hl.id);
continue;
}
foreach (var action in actionsForFeed)
{
Console.WriteLine($" {hl.title} -> action: {action.command}");
var noteString = $"{hl.note}\n[{DateTime.Now.ToLongTimeString()}] sponsorcheck: {sponsorCheck.Item2}";
ttrss.datastructures.Label nameLabel;
string podcastName;
if (!string.IsNullOrWhiteSpace(noteString))
{
noteString += $"{hl.note}\n";
}
switch (action.command)
{
case "dl":
var stdDLResult = await standardDL(hl.link.ToString());
await ttrssClient.UpdateArticleNote($"{noteString}[{DateTime.Now.ToLongTimeString()}] - {stdDLResult.Item2}", hl.id);
if (stdDLResult.Item1 == true)
{
Console.WriteLine($" {hl.title} -> dl success, removing label");
await ttrssClient.SetArticleLabel(
labelsWRTFeed.First(l => l.caption == action.triggerlabelCaption).id,
false,
hl.id);
Phase1Tasks.Add(appropriatePhase1Task.ActOn(hl, labelsWRTArticle));
}
else
{
Console.WriteLine($" {hl.title} -> dl failed");
Console.Error.WriteLine($"couldn't find phase 1 task {action.command} for workorder referring to article id {hl.id}!");
}
break;
case "podcastifyYT":
nameLabel = labelsWRTFeed.FirstOrDefault(l => l.caption?.StartsWith(conf.PodcastTitlePrefix) == true && l.@checked);
podcastName = nameLabel?.caption.Substring(conf.PodcastTitlePrefix.Length)
?? hl.feed_title;
var YTpodcastifyResult = await podcastifyYT(hl.link.ToString(), podcastName);
await ttrssClient.UpdateArticleNote($"{noteString}[{DateTime.Now.ToLongTimeString()}] - {YTpodcastifyResult.Item2}", hl.id);
if (YTpodcastifyResult.Item1 == true)
{
Console.WriteLine($" {hl.title} -> podcastify (YT) success, removing labels");
await ttrssClient.SetArticleLabel(
labelsWRTFeed.First(l => l.caption == action.triggerlabelCaption).id,
false,
hl.id);
if (nameLabel != null)
{
await ttrssClient.SetArticleLabel(nameLabel.id, false, hl.id);
}
}
}
Console.WriteLine($"done processing feeds. phase 1 tasks launched. wait to complete.");
var remainingWork = new List<WorkOrder>();
foreach(var lingeringTask in Phase1Tasks)
{
var wo = await lingeringTask;
if(wo != null)
{
Console.WriteLine($"articleId {wo.articleId} left a work order; it has {wo.Phase2TaskList.Count()} phase 2 task{(wo.Phase2TaskList.Count() == 1 ? "" : "s")}");
remainingWork.Add(wo);
}
}
Console.WriteLine($"phase 1 tasks complete, carrying {remainingWork.Count()} workorder{(remainingWork.Count() == 1 ? "" : "s")} to phase 2");
#endregion
#region local tasks (stripping ads, converting.)
Console.WriteLine("===== phase 2 =====");
//loop through working directory looking for other work orders to add
var workOrderFiles = Directory.GetFiles($"{conf.WorkingDirectory}", "workorder.json", new EnumerationOptions(){RecurseSubdirectories = true});
Console.WriteLine($"{workOrderFiles.Count()} workorder file{(workOrderFiles.Count() == 1 ? "" : "s")} located");
foreach(var workorderFile in workOrderFiles)
{
try
{
remainingWork.Add(JsonConvert.DeserializeObject<WorkOrder>(workorderFile));
Console.WriteLine($"picked up workorder task; {workorderFile}");
}
catch(Exception e)
{
Console.Error.WriteLine($"error picking up work order file {workorderFile} - {e.Message}; {e.StackTrace}");
}
}
Console.WriteLine($"{remainingWork.Count} phase 2 workorders, between pulled from ttrss and read from files");
while(remainingWork.Count > 0)
{
//todo: solve the halting problem
//ok but seriously,
Phase2Task.TtrssClient = ttrssClient;
Phase2Task.Conf = conf;
var phase2TaskTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(domainAssembly => domainAssembly.GetTypes())
.Where(type => type.IsSubclassOf(typeof(Phase2Task)) && !type.IsAbstract)
.ToList();
var phase2TaskConcretions = new List<Phase2Task>();
foreach (var phase2TaskType in phase2TaskTypes)
{
var concretion = (Phase2Task) Activator.CreateInstance(phase2TaskType);
phase2TaskConcretions.Add(concretion);
}
Console.WriteLine($"{phase2TaskConcretions.Count()} phase 2 task types understood");
var Phase2Tasks = new List<Task<Tuple<Phase2Task.TaskStatus, WorkOrder>>>();
Console.WriteLine($"launching first pass over work orders in phase 2.");
foreach(var wo in remainingWork)
{
var taskName = wo.Phase2TaskList[wo.Phase2TaskList.Keys.Min()];
var appropriatePhase2Task = phase2TaskConcretions.FirstOrDefault(tt => tt.TaskName == taskName);
if(appropriatePhase2Task != null)
{
wo.Phase2TaskList.Remove(wo.Phase2TaskList.Keys.Min());
Phase2Tasks.Add(appropriatePhase2Task.ActOn(wo));
}
else
{
Console.WriteLine($" {hl.title} -> podcastify (YT) failed");
}
break;
case "podcastifyAttachment":
nameLabel = labelsWRTFeed.FirstOrDefault(l => l.caption?.StartsWith(conf.PodcastTitlePrefix) == true && l.@checked);
podcastName = nameLabel?.caption.Substring(conf.PodcastTitlePrefix.Length)
?? hl.feed_title;
var attachmentLink = hl.attachments.Select(a => a.content_url)?.FirstOrDefault();
var ATTpodcastifyResult = await podcastifyAttachment(attachmentLink.ToString(), podcastName, hl.title);
await ttrssClient.UpdateArticleNote($"{noteString}[{DateTime.Now.ToLongTimeString()}] - {ATTpodcastifyResult.Item2}", hl.id);
if (ATTpodcastifyResult.Item1 == true)
{
Console.WriteLine($" {hl.title} -> podcastify (att) success, removing labels");
await ttrssClient.SetArticleLabel(
labelsWRTFeed.First(l => l.caption == action.triggerlabelCaption).id,
false,
hl.id);
if (nameLabel != null)
{
await ttrssClient.SetArticleLabel(nameLabel.id, false, hl.id);
}
}
else
{
Console.WriteLine($" {hl.title} -> podcastify (att) failed");
}
break;
default:
noteString += $"[{DateTime.Now.ToLongTimeString()}] - feed configured but action not recognized";
break;
}
}
}
Console.Error.WriteLine($"couldn't find phase 2 task {taskName} for workorder referring to article id {wo.articleId}!");
}
}
Console.WriteLine($"phase 2 tasks launched. now the complex part.");
Console.WriteLine($"logging out");
remainingWork = new List<WorkOrder>();
foreach(var lingeringTask in Phase2Tasks)
{
var wo = await lingeringTask;
switch (wo.Item1)
{
case Phase2Task.TaskStatus.Done:
// :)
break;
case Phase2Task.TaskStatus.ContinueNow:
remainingWork.Add(wo.Item2);
break;
case Phase2Task.TaskStatus.TryLater:
File.WriteAllText(Path.Combine(conf.WorkingDirectory, wo.Item2.guid.ToString(), "workorder.json"), JsonConvert.SerializeObject(wo));
break;
}
}
Console.WriteLine($"{remainingWork.Count} phase 2 tasks not complete - should be picked up next run..");
}
#endregion
await ttrssClient.Logout();
Console.WriteLine($"done, moving files from temporary location");
Console.WriteLine($"logged out of ttrss.");
var resulted = Directory.GetFiles("tmp", "*.*", SearchOption.AllDirectories);
if (resulted.Count() > 0)
{
foreach (var f in resulted)
{
var moveTarget = Path.Combine(conf.OnDoneCopy, f.Substring("tmp/".Length));
if (!Path.Exists(Path.GetDirectoryName(moveTarget)))
{
Directory.CreateDirectory(Path.GetDirectoryName(moveTarget));
}
if (File.Exists(moveTarget))
{
Console.WriteLine("file already exists, abandoning");
}
else
{
File.Move(f, moveTarget);
}
}
}
Console.WriteLine($"done for real");
}
static Configuration Configure(string configurationPath = "appsettings.json")
@ -174,49 +205,6 @@ namespace ttrss_co_client
return conf;
}
private static async Task<Tuple<bool, string>> standardDL(string articleLink)
{
Console.WriteLine($" standard downloading {articleLink}");
try
{
var ytdl = new YoutubeDLSharp.YoutubeDL();
ytdl.YoutubeDLPath = "yt-dlp";
ytdl.FFmpegPath = "ffmpeg";
ytdl.OutputFolder = "./tmp/recent episodes";
ytdl.OutputFileTemplate = "%(upload_date)s - %(title)s - [%(id)s].%(ext)s";
var sw = new Stopwatch();
sw.Start();
var res = await ytdl.RunVideoDownload(articleLink);
sw.Stop();
var outputStr = $"{(res.Success ? "Success" : "fail")} in {sw.Elapsed}";
if (res.ErrorOutput != null && res.ErrorOutput.Length > 0)
{
outputStr += "\n" + string.Join('\n', res.ErrorOutput);
}
Console.WriteLine($" download {(res.Success ? "success" : "failed")}: {articleLink} -> {res.Data}");
if (!res.Data.EndsWith(".mp4"))
{
Console.WriteLine(" must convert video");
sw.Reset();
var outputFilename = res.Data.Substring(0, res.Data.LastIndexOf('.')) + ".mp4";
sw.Start();
var conversionProc = Process.Start("ffmpeg", $"-y -i \"{res.Data}\" \"{outputFilename}\"");
conversionProc.WaitForExit();
sw.Stop();
Console.WriteLine($" converted {res.Data} -> {outputFilename}");
File.Delete(res.Data);
outputStr += $"\nconverted in {sw.Elapsed}";
}
return new Tuple<bool, string>(res.Success, outputStr);
}
catch (Exception e)
{
Console.Error.WriteLine($"{e.ToString()}: {e.Message}.\n{e.StackTrace}");
return new Tuple<bool, string>(false, $"{e.ToString()}: {e.Message}.\n{e.StackTrace}");
}
}
private static async Task<Tuple<bool, string>> podcastifyYT(string articleLink, string podcastName)
{
Console.WriteLine($" youtube-podcastifying {articleLink} ({podcastName})");
@ -296,50 +284,50 @@ namespace ttrss_co_client
return new Tuple<bool, string>(false, $"{e.ToString()}: {e.Message}.\n{e.StackTrace}");
}
}
private static async Task<Tuple<bool, string>> SponsorCheck(ttrss.datastructures.Headline hl)
{
if (!hl.link.Host.EndsWith("youtube.com"))
{
return new Tuple<bool, string>(true, "sponsorblock, sadly, only exists for youtube");
}
var match = Regex.Match(hl.link.Query, "v=([^&]+)(&|$)");
var videoId = match.Groups?[1].Value;
var c = new HttpClient();
// private static async Task<Tuple<bool, string>> SponsorCheck(ttrss.datastructures.Headline hl)
// {
// if (!hl.link.Host.EndsWith("youtube.com"))
// {
// return new Tuple<bool, string>(true, "sponsorblock, sadly, only exists for youtube");
// }
// var match = Regex.Match(hl.link.Query, "v=([^&]+)(&|$)");
// var videoId = match.Groups?[1].Value;
// var c = new HttpClient();
var sponsorblockcheck = await c.GetAsync($"https://sponsor.ajay.app/api/skipSegments?videoID={videoId}&category=sponsor&category=selfpromo&category=interaciton&category=intro&category=outro&category=preview");
if (sponsorblockcheck.StatusCode == System.Net.HttpStatusCode.NotFound)
{
Console.WriteLine($"sponsorblock reports that {videoId} has no entries (yet)");
var updateTimestamp = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
updateTimestamp = updateTimestamp.AddSeconds(hl.updated).ToLocalTime();
if (DateTime.Now - updateTimestamp > TimeSpan.FromMinutes(45))
{
return new Tuple<bool, string>(true, $"updated {updateTimestamp} (more than 45 minutes ago), going to give up waiting for sponsorblock");
}
else
{
return new Tuple<bool, string>(false, "none found, waiting");
}
}
else
{
try
{
var segments = JsonConvert.DeserializeObject<IEnumerable<sponsorblock.Segment>>(await sponsorblockcheck.Content.ReadAsStringAsync());
if (segments.Count() > 1)
{
return new Tuple<bool, string>(true, $"{segments.Count()} segments");
}
else
{
return new Tuple<bool, string>(false, $"no segments");
}
}
catch (Exception e)
{
return new Tuple<bool, string>(false, $"{e.ToString()} - {e.Message}");
}
}
}
// var sponsorblockcheck = await c.GetAsync($"https://sponsor.ajay.app/api/skipSegments?videoID={videoId}&category=sponsor&category=selfpromo&category=interaciton&category=intro&category=outro&category=preview");
// if (sponsorblockcheck.StatusCode == System.Net.HttpStatusCode.NotFound)
// {
// Console.WriteLine($"sponsorblock reports that {videoId} has no entries (yet)");
// var updateTimestamp = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
// updateTimestamp = updateTimestamp.AddSeconds(hl.updated).ToLocalTime();
// if (DateTime.Now - updateTimestamp > TimeSpan.FromMinutes(45))
// {
// return new Tuple<bool, string>(true, $"updated {updateTimestamp} (more than 45 minutes ago), going to give up waiting for sponsorblock");
// }
// else
// {
// return new Tuple<bool, string>(false, "none found, waiting");
// }
// }
// else
// {
// try
// {
// var segments = JsonConvert.DeserializeObject<IEnumerable<sponsorblock.Segment>>(await sponsorblockcheck.Content.ReadAsStringAsync());
// if (segments.Count() > 1)
// {
// return new Tuple<bool, string>(true, $"{segments.Count()} segments");
// }
// else
// {
// return new Tuple<bool, string>(false, $"no segments");
// }
// }
// catch (Exception e)
// {
// return new Tuple<bool, string>(false, $"{e.ToString()} - {e.Message}");
// }
// }
// }
}
}

View File

@ -4,6 +4,7 @@
"password": "sordph1sh",
"podcastTitlePrefix": "[podcast title] - ",
"onDoneCopy":"./",
"workingDirectory":"working/",
"feedActions":
[
{

15
tasks/Convert.cs Normal file
View File

@ -0,0 +1,15 @@
// using System.Diagnostics;
// using ttrss_co_client.ttrss;
// using ttrss_co_client.ttrss.datastructures;
// namespace ttrss_co_client.tasks
// {
// ///<summary>ffmpegify</summary>
// public class Convert : Phase2Task
// {
// public override async Task<WorkOrder> ActOn(WorkOrder wo)
// {
// }
// }
// }

15
tasks/Phase1Task.cs Normal file
View File

@ -0,0 +1,15 @@
using ttrss_co_client.ttrss;
using ttrss_co_client.ttrss.datastructures;
namespace ttrss_co_client.tasks
{
///<summary>generally, download</summary>
public abstract class Phase1Task
{
public virtual string TaskName { get { return this.GetType().ToString(); } }
public static ApiClient TtrssClient { get; set; }
public static Configuration Conf { get; set; }
public virtual string TriggerLabel { get; set; }
public abstract Task<WorkOrder> ActOn(Headline headline, IEnumerable<Label> labels);
}
}

16
tasks/Phase2Task.cs Normal file
View File

@ -0,0 +1,16 @@
using ttrss_co_client.ttrss;
using ttrss_co_client.ttrss.datastructures;
namespace ttrss_co_client.tasks
{
///<summary>convert, rip ads, etc</summary>
public abstract class Phase2Task
{
public string TaskName { get { return this.GetType().ToString(); } }
public static ApiClient TtrssClient { get; set; }
public static Configuration Conf { get; set; }
public enum TaskStatus {Done, ContinueNow, TryLater}
public abstract Task<Tuple<TaskStatus, WorkOrder>> ActOn(WorkOrder taskSpec);
}
}

View File

@ -0,0 +1,102 @@
// using System.Diagnostics;
// using ttrss_co_client.ttrss;
// using ttrss_co_client.ttrss.datastructures;
// namespace ttrss_co_client.tasks
// {
// ///<summary>download directly from an RSS attachment</summary>
// public class PodcastifyAttachment : Phase1Task
// {
// public override async Task<WorkOrder> ActOn(Headline headline, IEnumerable<Label> labelsWRTArticle)
// {
// // case "podcastifyAttachment":
// // nameLabel = labelsWRTArticle.FirstOrDefault(l => l.caption?.StartsWith(conf.PodcastTitlePrefix) == true && l.@checked);
// // podcastName = nameLabel?.caption.Substring(conf.PodcastTitlePrefix.Length)
// // ?? hl.feed_title;
// // var attachmentLink = hl.attachments.Select(a => a.content_url)?.FirstOrDefault();
// // var ATTpodcastifyResult = await podcastifyAttachment(attachmentLink.ToString(), podcastName, hl.title);
// // await ttrssClient.UpdateArticleNote($"{noteString}[{DateTime.Now.ToLongTimeString()}] - {ATTpodcastifyResult.Item2}", hl.id);
// // if (ATTpodcastifyResult.Item1 == true)
// // {
// // Console.WriteLine($" {hl.title} -> podcastify (att) success, removing labels");
// // await ttrssClient.SetArticleLabel(
// // labelsWRTArticle.First(l => l.caption == action.triggerlabelCaption).id,
// // false,
// // hl.id);
// // if (nameLabel != null)
// // {
// // await ttrssClient.SetArticleLabel(nameLabel.id, false, hl.id);
// // }
// // }
// // else
// // {
// // Console.WriteLine($" {hl.title} -> podcastify (att) failed");
// // }
// // break;
// // Console.WriteLine($" youtube podcastify: {headline.link.ToString()}");
// // var myGuid = Guid.NewGuid();
// // var ytdl = new YoutubeDLSharp.YoutubeDL();
// // ytdl.YoutubeDLPath = "yt-dlp";
// // ytdl.FFmpegPath = "ffmpeg";
// // ytdl.OutputFolder = $"{Conf.WorkingDirectory}/{myGuid}";
// // ytdl.OutputFileTemplate = "%(upload_date)s - %(title)s - [%(id)s].%(ext)s";
// // var sw = new Stopwatch();
// // try
// // {
// // sw.Start();
// // var res = await ytdl.RunVideoDownload(headline.link.ToString(), overrideOptions: new YoutubeDLSharp.Options.OptionSet() { });
// // sw.Stop();
// // var outputStr = $"download {(res.Success ? "success" : "fail")} in {sw.Elapsed}";
// // if (res.ErrorOutput != null && res.ErrorOutput.Length > 0)
// // {
// // outputStr += "\n" + string.Join('\n', res.ErrorOutput);
// // }
// // Console.WriteLine($" {headline.link} -> {res.Data}\n{outputStr}");
// // await TtrssClient.SetArticleLabel(labelsWRTArticle.First(l => l.caption?.ToLower() == this.TriggerLabel.ToLower()).id, false, headline.id);
// // Console.WriteLine($" {headline.title}: label removed");
// // if (!res.Success)
// // {
// // await TtrssClient.UpdateArticleNote($"{headline.note}\n[{DateTime.Now.ToLongTimeString()}] - standard dl failed; {string.Join('\n', res.ErrorOutput)}", hl.id);
// // return null;
// // }
// // else
// // {
// // var outputFilename = res.Data;
// // Console.WriteLine($"{headline.title} downloaded, shipping off to conversion. That task can determine if there's no work.");
// // var toReturn = new WorkOrder()
// // {
// // articleId = headline.id, //<-- that way adblocker, if I should want to suppress it, can be labelled on ttrss
// // nextTask = "convert",
// // data = new Dictionary<string, string>()
// // };
// // toReturn.data["path"] = outputFilename;
// // toReturn.data["target"] = outputFilename.Substring(0, res.Data.LastIndexOf('.')) + ".mp4";
// // return toReturn;
// // // sw.Reset();
// // // outputFilename =
// // // sw.Start();
// // // var conversionProc = Process.Start("ffmpeg", $"-y -i \"{res.Data}\" \"{outputFilename}\"");
// // // conversionProc.WaitForExit();
// // // sw.Stop();
// // // Console.WriteLine($" converted {res.Data} -> {outputFilename}");
// // // File.Delete(res.Data);
// // //outputStr += $"\nconverted in {sw.Elapsed}";
// // }
// // }
// // catch (Exception e)
// // {
// // Console.Error.WriteLine($"fatal error in standard DL for {headline.link}");
// // Console.Error.WriteLine($"{e.ToString()}: {e.Message}.\n{e.StackTrace}");
// // await TtrssClient.UpdateArticleNote($"{headline.note}\n[{DateTime.Now.ToLongTimeString()}] - fatal error {e.ToString()}: {e.Message}.\n{e.StackTrace}", hl.id);
// // return null;
// // }
// }
// }
// }

99
tasks/PodcastifyYT.cs Normal file
View File

@ -0,0 +1,99 @@
// using System.Diagnostics;
// using ttrss_co_client.ttrss;
// using ttrss_co_client.ttrss.datastructures;
// namespace ttrss_co_client.tasks
// {
// ///<summary>download from YT, to be ripped into a podcast</summary>
// public class PodcastifyYT : Phase1Task
// {
// public override async Task<WorkOrder> ActOn(Headline headline, IEnumerable<Label> labelsWRTArticle)
// {
// // case "podcastifyYT":
// // nameLabel = labelsWRTArticle.FirstOrDefault(l => l.caption?.StartsWith(conf.PodcastTitlePrefix) == true && l.@checked);
// // podcastName = nameLabel?.caption.Substring(conf.PodcastTitlePrefix.Length)
// // ?? hl.feed_title;
// // var YTpodcastifyResult = await podcastifyYT(hl.link.ToString(), podcastName);
// // await ttrssClient.UpdateArticleNote($"{noteString}[{DateTime.Now.ToLongTimeString()}] - {YTpodcastifyResult.Item2}", hl.id);
// // if (YTpodcastifyResult.Item1 == true)
// // {
// // Console.WriteLine($" {hl.title} -> podcastify (YT) success, removing labels");
// // await ttrssClient.SetArticleLabel(
// // labelsWRTArticle.First(l => l.caption == action.triggerlabelCaption).id,
// // false,
// // hl.id);
// // if (nameLabel != null)
// // {
// // await ttrssClient.SetArticleLabel(nameLabel.id, false, hl.id);
// // }
// // }
// // else
// // {
// // Console.WriteLine($" {hl.title} -> podcastify (YT) failed");
// // }
// // break;
// // Console.WriteLine($" youtube podcastify: {headline.link.ToString()}");
// // var myGuid = Guid.NewGuid();
// // var ytdl = new YoutubeDLSharp.YoutubeDL();
// // ytdl.YoutubeDLPath = "yt-dlp";
// // ytdl.FFmpegPath = "ffmpeg";
// // ytdl.OutputFolder = $"{Conf.WorkingDirectory}/{myGuid}";
// // ytdl.OutputFileTemplate = "%(upload_date)s - %(title)s - [%(id)s].%(ext)s";
// // var sw = new Stopwatch();
// // try
// // {
// // sw.Start();
// // var res = await ytdl.RunVideoDownload(headline.link.ToString(), overrideOptions: new YoutubeDLSharp.Options.OptionSet() { });
// // sw.Stop();
// // var outputStr = $"download {(res.Success ? "success" : "fail")} in {sw.Elapsed}";
// // if (res.ErrorOutput != null && res.ErrorOutput.Length > 0)
// // {
// // outputStr += "\n" + string.Join('\n', res.ErrorOutput);
// // }
// // Console.WriteLine($" {headline.link} -> {res.Data}\n{outputStr}");
// // await TtrssClient.SetArticleLabel(labelsWRTArticle.First(l => l.caption?.ToLower() == this.TriggerLabel.ToLower()).id, false, headline.id);
// // Console.WriteLine($" {headline.title}: label removed");
// // if (!res.Success)
// // {
// // await TtrssClient.UpdateArticleNote($"{headline.note}\n[{DateTime.Now.ToLongTimeString()}] - standard dl failed; {string.Join('\n', res.ErrorOutput)}", hl.id);
// // return null;
// // }
// // else
// // {
// // var outputFilename = res.Data;
// // Console.WriteLine($"{headline.title} downloaded, shipping off to conversion. That task can determine if there's no work.");
// // var toReturn = new WorkOrder()
// // {
// // articleId = headline.id, //<-- that way adblocker, if I should want to suppress it, can be labelled on ttrss
// // nextTask = "convert",
// // data = new Dictionary<string, string>()
// // };
// // toReturn.data["path"] = outputFilename;
// // toReturn.data["target"] = outputFilename.Substring(0, res.Data.LastIndexOf('.')) + ".mp4";
// // return toReturn;
// // // sw.Reset();
// // // outputFilename =
// // // sw.Start();
// // // var conversionProc = Process.Start("ffmpeg", $"-y -i \"{res.Data}\" \"{outputFilename}\"");
// // // conversionProc.WaitForExit();
// // // sw.Stop();
// // // Console.WriteLine($" converted {res.Data} -> {outputFilename}");
// // // File.Delete(res.Data);
// // //outputStr += $"\nconverted in {sw.Elapsed}";
// // }
// // }
// // catch (Exception e)
// // {
// // Console.Error.WriteLine($"fatal error in standard DL for {headline.link}");
// // Console.Error.WriteLine($"{e.ToString()}: {e.Message}.\n{e.StackTrace}");
// // await TtrssClient.UpdateArticleNote($"{headline.note}\n[{DateTime.Now.ToLongTimeString()}] - fatal error {e.ToString()}: {e.Message}.\n{e.StackTrace}", hl.id);
// // return null;
// // }
// }
// }
// }

25
tasks/Publish.cs Normal file
View File

@ -0,0 +1,25 @@
using System.Linq;
using System.Diagnostics;
using ttrss_co_client.ttrss;
using ttrss_co_client.ttrss.datastructures;
namespace ttrss_co_client.tasks
{
///<summary>Move to output</summary>
public class Publish : Phase2Task
{
public override async Task<Tuple<TaskStatus, WorkOrder>>ActOn(WorkOrder workOrder)
{
var wo = workOrder as PublishWorkOrder;
File.Move(wo.Path, wo.PublishTarget, true);
var article = (await TtrssClient.GetArticles(wo.articleId))?.FirstOrDefault();
await TtrssClient.UpdateArticleNote($"{article.note}\n[{DateTime.Now.ToShortDateString()}] - copied");
return new Tuple<TaskStatus, WorkOrder>(TaskStatus.Done, wo);
}
}
public class PublishWorkOrder : WorkOrder
{
public string Path => data["path"];
public string PublishTarget => data["publish-target"];
}
}

15
tasks/Sponsorblock.cs Normal file
View File

@ -0,0 +1,15 @@
// using System.Diagnostics;
// using ttrss_co_client.ttrss;
// using ttrss_co_client.ttrss.datastructures;
// namespace ttrss_co_client.tasks
// {
// ///<summary>Move to output</summary>
// public class Sponsorblock : Phase2Task
// {
// public override async Task<WorkOrder> ActOn(WorkOrder wo)
// {
// }
// }
// }

88
tasks/StandardDL.cs Normal file
View File

@ -0,0 +1,88 @@
using System.Diagnostics;
using ttrss_co_client.ttrss;
using ttrss_co_client.ttrss.datastructures;
namespace ttrss_co_client.tasks
{
///<summary>download from YT</summary>
public class StandardDL : Phase1Task
{
public override string TaskName => "dl";
public override async Task<WorkOrder> ActOn(Headline headline, IEnumerable<Label> labelsWRTArticle)
{
Console.WriteLine($" standard download: {headline.link.ToString()}");
var myGuid = Guid.NewGuid();
var ytdl = new YoutubeDLSharp.YoutubeDL();
ytdl.YoutubeDLPath = "yt-dlp";
ytdl.FFmpegPath = "ffmpeg";
ytdl.OutputFolder = $"{Conf.WorkingDirectory}/{myGuid}";
ytdl.OutputFileTemplate = "%(upload_date)s - %(title)s - [%(id)s].%(ext)s";
var sw = new Stopwatch();
try
{
sw.Start();
var res = await ytdl.RunVideoDownload(headline.link.ToString());
sw.Stop();
var outputStr = $"download {(res.Success ? "success" : "fail")} in {sw.Elapsed}";
if (res.ErrorOutput != null && res.ErrorOutput.Length > 0)
{
outputStr += "\n" + string.Join('\n', res.ErrorOutput);
}
Console.WriteLine($" {headline.link} -> {res.Data}\n{outputStr}");
await TtrssClient.SetArticleLabel(labelsWRTArticle.First(l => l.caption?.ToLower() == this.TriggerLabel.ToLower()).id, false, headline.id);
Console.WriteLine($" {headline.title}: label removed");
if (!res.Success)
{
await TtrssClient.UpdateArticleNote($"{headline.note}\n[{DateTime.Now.ToLongTimeString()}] - standard dl failed; {string.Join('\n', res.ErrorOutput)}", headline.id);
return null;
}
else
{
var outputFilename = res.Data;
Console.WriteLine($"{headline.title} downloaded.");
var toReturn = new WorkOrder()
{
articleId = headline.id,//<-- that way later tasks can update the note
Phase2TaskList = new Dictionary<int, string>(),
data = new Dictionary<string, string>()
};
toReturn.data["path"] = outputFilename;
if(!outputFilename.EndsWith(".mp4"))
{
Console.WriteLine($"{headline.title} needs conversion task.");
toReturn.Phase2TaskList[0] = "convert";
toReturn.data["conversion-target"] = outputFilename.Substring(0, res.Data.LastIndexOf('.')) + ".mp4";
}
if(headline.link.Host.EndsWith("youtube.com"))
{
toReturn.Phase2TaskList[1] = "sponsorblock";
}
toReturn.Phase2TaskList[2] = "filemovePublish";
toReturn.data["publish-target"] = $"{Conf.OnDoneCopy}/recent episodes/{toReturn.data["conversion-target"]}";
return toReturn;
// sw.Reset();
// outputFilename =
// sw.Start();
// var conversionProc = Process.Start("ffmpeg", $"-y -i \"{res.Data}\" \"{outputFilename}\"");
// conversionProc.WaitForExit();
// sw.Stop();
// Console.WriteLine($" converted {res.Data} -> {outputFilename}");
// File.Delete(res.Data);
//outputStr += $"\nconverted in {sw.Elapsed}";
}
}
catch (Exception e)
{
Console.Error.WriteLine($"fatal error in standard DL for {headline.link}");
Console.Error.WriteLine($"{e.ToString()}: {e.Message}.\n{e.StackTrace}");
await TtrssClient.UpdateArticleNote($"{headline.note}\n[{DateTime.Now.ToLongTimeString()}] - fatal error {e.ToString()}: {e.Message}.\n{e.StackTrace}", headline.id);
return null;
}
}
}
}

12
tasks/WorkOrder.cs Normal file
View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace ttrss_co_client.tasks
{
public class WorkOrder
{
public int articleId { get; set; }
public Guid guid { get; set; }
public Dictionary<int, string> Phase2TaskList { get; set; } //so youtube downloads will download, then add a conversion to this, then add an adblock, then publish
public Dictionary<string, string> data { get; set; }
}
}