Skip to content

Commit df089a3

Browse files
committed
Bump all plugins to v3.0.0
1 parent 2bccc91 commit df089a3

40 files changed

Lines changed: 1085 additions & 38 deletions

File tree

‎plugins/ChannelFlow.Plugin.AIProviderSuite/AIProviderSuitePlugin.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace ChannelFlow.Plugin.AIProviderSuite;
77

88
public class AIProviderSuitePlugin : IPlugin
99
{
10-
public PluginManifest Metadata => new("com.channelflow.ai", "AI Provider Suite", "2.0.11", "3.0.0", Category: "ai");
10+
public PluginManifest Metadata => new("com.channelflow.ai", "AI Provider Suite", "3.0.0", "3.0.0", Category: "ai");
1111
private PluginApi? _api;
1212
public Task OnLoadAsync(PluginApi api) { _api = api; return Task.CompletedTask; }
1313
public Task OnEnableAsync() => Task.CompletedTask;

‎plugins/ChannelFlow.Plugin.ErsatzTv/ErsatzTvPlugin.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public async Task<bool> HealthAsync()
140140

141141
public class ErsatzTvPlugin : IPlugin
142142
{
143-
public PluginManifest Metadata => new("com.channelflow.ersatztv", "ErsatzTV", "1.0.0", "3.0.0", Category: "playout");
143+
public PluginManifest Metadata => new("com.channelflow.ersatztv", "ErsatzTV", "3.0.0", "3.0.0", Category: "playout");
144144

145145
private PluginApi? _api;
146146
private ErsatzTvSupervisor? _sup;

‎plugins/ChannelFlow.Plugin.Jellyfin/JellyfinPlugin.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public async Task<SyncReport> SyncLibraryAsync(SyncContext ctx)
128128

129129
public class JellyfinPlugin : IPlugin
130130
{
131-
public PluginManifest Metadata => new("com.channelflow.jellyfin", "Jellyfin", "1.0.0", "3.0.0", Category: "media-source");
131+
public PluginManifest Metadata => new("com.channelflow.jellyfin", "Jellyfin", "3.0.0", "3.0.0", Category: "media-source");
132132
private PluginApi? _api;
133133
private JellyfinMediaSource? _ms;
134134
public Task OnLoadAsync(PluginApi api) { _api = api; _ms = new JellyfinMediaSource(); return Task.CompletedTask; }

‎plugins/ChannelFlow.Plugin.LocalFiles/LocalFilesPlugin.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace ChannelFlow.Plugin.LocalFiles;
77

88
public class LocalFilesPlugin : IPlugin
99
{
10-
public PluginManifest Metadata => new("com.channelflow.local", "Local Files", "2.0.5", "3.0.0", Category: "catalog");
10+
public PluginManifest Metadata => new("com.channelflow.local", "Local Files", "3.0.0", "3.0.0", Category: "catalog");
1111
private PluginApi? _api;
1212
public Task OnLoadAsync(PluginApi api) { _api = api; return Task.CompletedTask; }
1313
public Task OnEnableAsync() => Task.CompletedTask;

‎plugins/ChannelFlow.Plugin.PastTense/PastTensePlugin.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace ChannelFlow.Plugin.PastTense;
77

88
public class PastTensePlugin : IPlugin
99
{
10-
public PluginManifest Metadata => new("com.channelflow.pasttense", "Past Tense News", "2.0.3", "3.0.0", Category: "catalog");
10+
public PluginManifest Metadata => new("com.channelflow.pasttense", "Past Tense News", "3.0.0", "3.0.0", Category: "catalog");
1111
private PluginApi? _api;
1212
public Task OnLoadAsync(PluginApi api) { _api = api; return Task.CompletedTask; }
1313
public Task OnEnableAsync() => Task.CompletedTask;

‎plugins/ChannelFlow.Plugin.Scheduler/SchedulerPlugin.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public class SchedulerConfig
4242

4343
public class SchedulerPlugin : IPlugin
4444
{
45-
public PluginManifest Metadata => new("com.channelflow.scheduler", "Scheduler", "1.0.0", "3.0.0", Category: "scheduler");
45+
public PluginManifest Metadata => new("com.channelflow.scheduler", "Scheduler", "3.0.0", "3.0.0", Category: "scheduler");
4646

4747
private PluginApi? _api;
4848
private SchedulerConfig _config = new();
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
using System.Net.Http.Json;
2+
using System.Text.Json;
3+
using Microsoft.AspNetCore.Builder;
4+
using Microsoft.AspNetCore.Http;
5+
using ChannelFlow.PluginSdk;
6+
7+
namespace ChannelFlow.Plugin.AI;
8+
9+
public class AiProvider
10+
{
11+
public string Id { get; set; } = Guid.NewGuid().ToString();
12+
public string Name { get; set; } = "";
13+
public string BaseUrl { get; set; } = "https://api.openai.com/v1";
14+
public string ApiKey { get; set; } = "";
15+
public string Model { get; set; } = "gpt-4o-mini";
16+
public string? VisionModel { get; set; }
17+
public string? TtsModel { get; set; }
18+
public string? TtsVoice { get; set; }
19+
public double Temperature { get; set; } = 0.7;
20+
public bool Enabled { get; set; } = true;
21+
public int Priority { get; set; } = 0;
22+
public bool Ollama { get; set; } = false;
23+
}
24+
25+
public class AiPlugin : IPlugin
26+
{
27+
public PluginManifest Metadata => new("com.channelflow.ai", "AI Provider Suite", "3.0.0", "3.0.0", Category: "ai",
28+
Permissions: new List<string> { "storage:read", "storage:write", "network:outbound", "api:routes", "ui:pages" });
29+
30+
private PluginApi? _api;
31+
private readonly List<AiProvider> _providers = new();
32+
private readonly object _lock = new();
33+
34+
public Task OnLoadAsync(PluginApi api)
35+
{
36+
_api = api;
37+
// Load providers from storage
38+
_ = LoadProvidersAsync();
39+
return Task.CompletedTask;
40+
}
41+
42+
private async Task LoadProvidersAsync()
43+
{
44+
try
45+
{
46+
var val = await _api!.Storage.GetAsync("providers");
47+
if (val.HasValue && val.Value.ValueKind == JsonValueKind.Array)
48+
{
49+
lock (_lock)
50+
{
51+
_providers.Clear();
52+
foreach (var el in val.Value.EnumerateArray())
53+
{
54+
var p = JsonSerializer.Deserialize<AiProvider>(el.GetRawText(), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
55+
if (p is not null) _providers.Add(p);
56+
}
57+
}
58+
}
59+
}
60+
catch { }
61+
}
62+
63+
private async Task SaveProvidersAsync()
64+
{
65+
try
66+
{
67+
lock (_lock)
68+
{
69+
var json = JsonSerializer.SerializeToElement(_providers);
70+
_ = _api!.Storage.SetAsync("providers", json);
71+
}
72+
}
73+
catch { }
74+
}
75+
76+
public Task OnEnableAsync() => Task.CompletedTask;
77+
public Task OnDisableAsync() => Task.CompletedTask;
78+
public void OnUnload() { }
79+
public Task OnConfigAsync(JsonElement config) => Task.CompletedTask;
80+
81+
public void MapRoutes(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints)
82+
{
83+
// GET / - list providers
84+
endpoints.MapGet("/", () =>
85+
{
86+
lock (_lock)
87+
{
88+
var enabled = _providers.Where(p => p.Enabled).OrderBy(p => p.Priority).ToList();
89+
return Results.Ok(new
90+
{
91+
providers = _providers,
92+
chosen = enabled.FirstOrDefault()?.Id
93+
});
94+
}
95+
});
96+
97+
// POST /providers - create
98+
endpoints.MapPost("/providers", async (HttpRequest req) =>
99+
{
100+
using var reader = new StreamReader(req.Body);
101+
var json = await reader.ReadToEndAsync();
102+
try
103+
{
104+
var body = JsonSerializer.Deserialize<AiProvider>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
105+
if (body is null) return Results.BadRequest(new { error = "invalid body" });
106+
if (string.IsNullOrEmpty(body.Id)) body.Id = Guid.NewGuid().ToString();
107+
lock (_lock) { _providers.Add(body); }
108+
await SaveProvidersAsync();
109+
_api?.Logger.Info($"ai: created provider '{body.Name}'");
110+
return Results.Created($"/providers/{body.Id}", body);
111+
}
112+
catch (Exception e) { return Results.BadRequest(new { error = e.Message }); }
113+
});
114+
115+
// PUT /providers/{id} - update
116+
endpoints.MapPut("/providers/{id}", async (string id, HttpRequest req) =>
117+
{
118+
using var reader = new StreamReader(req.Body);
119+
var json = await reader.ReadToEndAsync();
120+
try
121+
{
122+
var body = JsonSerializer.Deserialize<AiProvider>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
123+
if (body is null) return Results.BadRequest(new { error = "invalid body" });
124+
lock (_lock)
125+
{
126+
var idx = _providers.FindIndex(p => p.Id == id);
127+
if (idx < 0) return Results.NotFound(new { error = $"no provider with id {id}" });
128+
body.Id = id;
129+
_providers[idx] = body;
130+
}
131+
await SaveProvidersAsync();
132+
return Results.Ok(body);
133+
}
134+
catch (Exception e) { return Results.BadRequest(new { error = e.Message }); }
135+
});
136+
137+
// DELETE /providers/{id}
138+
endpoints.MapDelete("/providers/{id}", async (string id) =>
139+
{
140+
lock (_lock)
141+
{
142+
var idx = _providers.FindIndex(p => p.Id == id);
143+
if (idx < 0) return Results.NotFound(new { error = $"no provider with id {id}" });
144+
_providers.RemoveAt(idx);
145+
}
146+
await SaveProvidersAsync();
147+
return Results.NoContent();
148+
});
149+
150+
// POST /providers/{id}/test - test a saved provider
151+
endpoints.MapPost("/providers/{id}/test", async (string id, HttpRequest req) =>
152+
{
153+
AiProvider? provider;
154+
lock (_lock) { provider = _providers.FirstOrDefault(p => p.Id == id); }
155+
if (provider is null) return Results.NotFound(new { error = $"no provider with id {id}" });
156+
157+
using var reader = new StreamReader(req.Body);
158+
var json = await reader.ReadToEndAsync();
159+
JsonElement body = default;
160+
if (!string.IsNullOrEmpty(json)) body = JsonSerializer.Deserialize<JsonElement>(json);
161+
162+
var report = await TestProviderAsync(provider, body);
163+
return Results.Ok(report);
164+
});
165+
166+
// POST /test - test a draft provider
167+
endpoints.MapPost("/test", async (HttpRequest req) =>
168+
{
169+
using var reader = new StreamReader(req.Body);
170+
var json = await reader.ReadToEndAsync();
171+
try
172+
{
173+
var provider = JsonSerializer.Deserialize<AiProvider>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
174+
if (provider is null) return Results.BadRequest(new { error = "invalid body" });
175+
var report = await TestProviderAsync(provider, default);
176+
return Results.Ok(report);
177+
}
178+
catch (Exception e) { return Results.BadRequest(new { error = e.Message }); }
179+
});
180+
181+
// POST /test-all - test all enabled providers in priority order
182+
endpoints.MapPost("/test-all", async () =>
183+
{
184+
List<AiProvider> enabled;
185+
lock (_lock) { enabled = _providers.Where(p => p.Enabled).OrderBy(p => p.Priority).ToList(); }
186+
var attempts = new List<object>();
187+
bool ok = false;
188+
string? chosen = null;
189+
foreach (var p in enabled)
190+
{
191+
var report = await TestProviderAsync(p, default);
192+
var probeOk = report.Ok;
193+
attempts.Add(new { id = p.Id, name = p.Name, ok = probeOk, detail = report.Detail });
194+
if (probeOk && !ok) { ok = true; chosen = p.Name; }
195+
}
196+
return Results.Ok(new { ok, chosen, attempts });
197+
});
198+
199+
// POST /models - fetch models from a provider
200+
endpoints.MapPost("/models", async (HttpRequest req) =>
201+
{
202+
using var reader = new StreamReader(req.Body);
203+
var json = await reader.ReadToEndAsync();
204+
JsonElement body = default;
205+
if (!string.IsNullOrEmpty(json)) body = JsonSerializer.Deserialize<JsonElement>(json);
206+
207+
var baseUrl = body.TryGetProperty("base_url", out var bu) ? bu.GetString() ?? "" : "";
208+
var apiKey = body.TryGetProperty("api_key", out var ak) ? ak.GetString() ?? "" : "";
209+
var ollama = body.TryGetProperty("ollama", out var ol) && ol.GetBoolean();
210+
211+
if (string.IsNullOrWhiteSpace(baseUrl))
212+
baseUrl = ollama ? "http://localhost:11434/v1" : "https://api.openai.com/v1";
213+
baseUrl = baseUrl.Trim().TrimEnd('/');
214+
215+
try
216+
{
217+
using var http = new HttpClient();
218+
var request = http.GetAsync($"{baseUrl}/models");
219+
if (!string.IsNullOrWhiteSpace(apiKey) && !ollama)
220+
http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", apiKey);
221+
222+
var res = await request;
223+
if (!res.IsSuccessStatusCode)
224+
return Results.BadRequest(new { error = $"provider answered {res.StatusCode}" });
225+
226+
var text = await res.Content.ReadAsStringAsync();
227+
using var doc = JsonDocument.Parse(text);
228+
var models = new List<string>();
229+
var arr = doc.RootElement.TryGetProperty("data", out var data) ? data : doc.RootElement;
230+
if (arr.ValueKind == JsonValueKind.Array)
231+
{
232+
foreach (var item in arr.EnumerateArray())
233+
{
234+
var name = item.TryGetProperty("id", out var id) ? id.GetString() : item.GetString();
235+
if (!string.IsNullOrEmpty(name)) models.Add(name!);
236+
}
237+
}
238+
models = models.Distinct().OrderBy(m => m).ToList();
239+
if (models.Count == 0) return Results.BadRequest(new { error = "provider returned no models" });
240+
241+
var voices = new[] { "alloy", "echo", "fable", "onyx", "nova", "shimmer", "ash", "ballad", "coral", "sage", "verse" };
242+
return Results.Ok(new { ok = true, base_url = baseUrl, models, voices });
243+
}
244+
catch (Exception e)
245+
{
246+
return Results.BadRequest(new { error = $"could not reach the provider: {e.Message}" });
247+
}
248+
});
249+
}
250+
251+
private async Task<TestReport> TestProviderAsync(AiProvider provider, JsonElement body)
252+
{
253+
var baseUrl = provider.BaseUrl.Trim().TrimEnd('/');
254+
if (!string.IsNullOrEmpty(provider.ApiKey))
255+
{
256+
// Override from body if provided
257+
if (body.ValueKind == JsonValueKind.Object)
258+
{
259+
if (body.TryGetProperty("base_url", out var bu) && !string.IsNullOrEmpty(bu.GetString()))
260+
baseUrl = bu.GetString()!.Trim().TrimEnd('/');
261+
if (body.TryGetProperty("api_key", out var ak) && !string.IsNullOrEmpty(ak.GetString()))
262+
provider.ApiKey = ak.GetString()!;
263+
}
264+
}
265+
266+
var model = provider.Model;
267+
if (body.ValueKind == JsonValueKind.Object && body.TryGetProperty("model", out var m) && !string.IsNullOrEmpty(m.GetString()))
268+
model = m.GetString()!;
269+
270+
try
271+
{
272+
using var http = new HttpClient();
273+
http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", provider.ApiKey);
274+
var sw = System.Diagnostics.Stopwatch.StartNew();
275+
var res = await http.PostAsJsonAsync($"{baseUrl}/chat/completions", new
276+
{
277+
model = model,
278+
messages = new[] { new { role = "user", content = "Say OK" } },
279+
max_tokens = 5
280+
});
281+
sw.Stop();
282+
var ok = res.IsSuccessStatusCode;
283+
var detail = ok ? $"responded in {sw.ElapsedMilliseconds}ms" : $"HTTP {(int)res.StatusCode}";
284+
return new TestReport(ok, detail);
285+
}
286+
catch (Exception e)
287+
{
288+
return new TestReport(false, e.Message);
289+
}
290+
}
291+
292+
public IEnumerable<UiContribution> UiContributions => Array.Empty<UiContribution>();
293+
public PluginHealth Health => new(true, "ok");
294+
public IMediaSource? MediaSource => null;
295+
}
296+
297+
public record TestReport(bool Ok, string Detail);
0 Bytes
Binary file not shown.

‎plugins/com.channelflow.ai/plugin.json‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@
44
"version": "3.0.0",
55
"minApi": "3.0.0",
66
"forceMinApiVersion": "3.0.0",
7-
"entrypoint": { "dll": "ChannelFlow.Plugin.AI.dll", "type": "ChannelFlow.Plugin.AI.AiPlugin" },
7+
"entrypoint": {
8+
"dll": "ChannelFlow.Plugin.AI.dll",
9+
"type": "ChannelFlow.Plugin.AI.AiPlugin"
10+
},
811
"category": "ai",
9-
"permissions": ["storage:read", "storage:write", "network:outbound", "api:routes", "ui:pages"]
10-
}
12+
"permissions": [
13+
"storage:read",
14+
"storage:write",
15+
"network:outbound",
16+
"api:routes",
17+
"ui:pages"
18+
]
19+
}
Binary file not shown.

0 commit comments

Comments
 (0)