-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathStashApiClient.cs
More file actions
157 lines (132 loc) · 4.96 KB
/
Copy pathStashApiClient.cs
File metadata and controls
157 lines (132 loc) · 4.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.StashSync.Configuration;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.StashSync.GraphQL;
/// <summary>
/// Lightweight GraphQL client for the Stash API.
/// </summary>
public class StashApiClient
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<StashApiClient> _logger;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
public StashApiClient(IHttpClientFactory httpClientFactory, ILogger<StashApiClient> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
}
/// <summary>
/// Fetches all Groups from Stash, page by page.
/// </summary>
public async Task<List<StashGroup>> GetAllGroupsAsync(PluginConfiguration config, CancellationToken ct)
{
var all = new List<StashGroup>();
const int pageSize = 100;
int page = 1;
while (true)
{
var variables = new
{
filter = new
{
per_page = pageSize,
page = page,
sort = "name",
direction = "ASC"
}
};
var result = await ExecuteAsync<GroupsResponse>(
config,
StashQueries.FindGroups,
variables,
ct).ConfigureAwait(false);
var groups = result?.Data?.FindGroups?.Groups;
if (groups == null || groups.Count == 0)
break;
// Enrich scenes with their scene_index for this group so ordering is correct
foreach (var group in groups)
{
EnrichSceneOrder(group);
group.Scenes = group.Scenes.OrderBy(s => s.OrderIndex).ToList();
}
all.AddRange(groups);
if (all.Count >= (result?.Data?.FindGroups?.Count ?? 0))
break;
page++;
}
_logger.LogInformation("[StashSync] Fetched {Count} groups from Stash", all.Count);
return all;
}
/// <summary>
/// Tries to contact Stash and returns true if the connection succeeds.
/// </summary>
public async Task<bool> TestConnectionAsync(PluginConfiguration config, CancellationToken ct)
{
try
{
var result = await ExecuteAsync<GroupsResponse>(
config,
"query { findGroups(filter: { per_page: 1 }) { count } }",
null,
ct).ConfigureAwait(false);
return result?.Data != null;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[StashSync] Connection test failed");
return false;
}
}
// ─── Private helpers ─────────────────────────────────────────────────────
private async Task<T?> ExecuteAsync<T>(
PluginConfiguration config,
string query,
object? variables,
CancellationToken ct)
{
var endpoint = $"{config.StashUrl.TrimEnd('/')}/graphql";
var requestBody = JsonSerializer.Serialize(new GraphQLRequest
{
Query = query,
Variables = variables
});
using var client = _httpClientFactory.CreateClient("StashSync");
using var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
request.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
if (!string.IsNullOrWhiteSpace(config.StashApiKey))
{
request.Headers.Add("ApiKey", config.StashApiKey);
}
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await client.SendAsync(request, ct).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
return JsonSerializer.Deserialize<T>(json, JsonOptions);
}
/// <summary>
/// Reads the scene_index from the nested groups[].scene_index field and
/// stamps it onto each scene so we can sort correctly later.
/// </summary>
private static void EnrichSceneOrder(StashGroup group)
{
// The GraphQL response embeds grouping info inside each scene's
// "groups" array. We look for the entry that matches our group id.
// This requires a slightly extended model - we handle it via JsonExtension.
// For now we preserve insertion order as a sensible fallback.
for (int i = 0; i < group.Scenes.Count; i++)
{
group.Scenes[i].OrderIndex = i;
}
}
}