feat: complete e621 integration with API key auth, comics/pools, post variations, and server sync (v3.7.0)
This commit is contained in:
@@ -36,6 +36,38 @@ class E621Mapper {
|
||||
for (final entry in tagsMap.entries)
|
||||
entry.key: List<String>.from((entry.value as List?) ?? const []),
|
||||
}..removeWhere((_, value) => value.isEmpty);
|
||||
|
||||
final relationships = Map<String, dynamic>.from((json['relationships'] as Map?) ?? const {});
|
||||
final parentId = relationships['parent_id']?.toString();
|
||||
if (parentId != null && parentId.isNotEmpty && parentId != 'null') {
|
||||
tagGroups['parent_id'] = [parentId];
|
||||
}
|
||||
final children = (relationships['children'] as List?)
|
||||
?.map((e) => e?.toString() ?? '')
|
||||
.where((s) => s.isNotEmpty && s != 'null')
|
||||
.toList();
|
||||
if (children != null && children.isNotEmpty) {
|
||||
tagGroups['children'] = children;
|
||||
}
|
||||
|
||||
final rawPools = (json['pools'] as List?)
|
||||
?.map((e) => e?.toString() ?? '')
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
if (rawPools != null && rawPools.isNotEmpty) {
|
||||
tagGroups['pools'] = rawPools;
|
||||
}
|
||||
|
||||
final favCount = json['fav_count'];
|
||||
if (favCount != null) {
|
||||
tagGroups['fav_count'] = [favCount.toString()];
|
||||
}
|
||||
|
||||
final desc = _string(json['description']);
|
||||
if (desc.isNotEmpty) {
|
||||
tagGroups['description'] = [desc];
|
||||
}
|
||||
|
||||
final tags = tagGroups.values.expand((items) => items).toSet().toList();
|
||||
final fileUrl = _string(file['url']);
|
||||
return Post(
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
class E621Pool {
|
||||
const E621Pool({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.postIds,
|
||||
required this.postCount,
|
||||
required this.category,
|
||||
required this.isActive,
|
||||
this.creatorName,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final String name;
|
||||
final String description;
|
||||
final List<String> postIds;
|
||||
final int postCount;
|
||||
final String category; // 'series' or 'collection'
|
||||
final bool isActive;
|
||||
final String? creatorName;
|
||||
|
||||
String get cleanTitle => name.replaceAll('_', ' ');
|
||||
|
||||
int pageOf(String postId) {
|
||||
final idx = postIds.indexOf(postId);
|
||||
return idx >= 0 ? idx + 1 : 0;
|
||||
}
|
||||
|
||||
String? nextPostId(String postId) {
|
||||
final idx = postIds.indexOf(postId);
|
||||
if (idx >= 0 && idx < postIds.length - 1) {
|
||||
return postIds[idx + 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? previousPostId(String postId) {
|
||||
final idx = postIds.indexOf(postId);
|
||||
if (idx > 0) {
|
||||
return postIds[idx - 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
factory E621Pool.fromJson(Map<String, dynamic> json) {
|
||||
final rawPostIds = (json['post_ids'] as List?) ?? const [];
|
||||
return E621Pool(
|
||||
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||
name: (json['name'] ?? '').toString(),
|
||||
description: (json['description'] ?? '').toString(),
|
||||
postIds: rawPostIds.map((e) => e.toString()).toList(),
|
||||
postCount: (json['post_count'] as num?)?.toInt() ?? rawPostIds.length,
|
||||
category: (json['category'] ?? 'series').toString(),
|
||||
isActive: (json['is_active'] as bool?) ?? true,
|
||||
creatorName: json['creator_name']?.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,27 @@ class Post {
|
||||
|
||||
String? get description => commentary;
|
||||
|
||||
String? get parentId {
|
||||
final list = tagGroups['parent_id'];
|
||||
return (list != null && list.isNotEmpty && list.first.trim().isNotEmpty)
|
||||
? list.first.trim()
|
||||
: null;
|
||||
}
|
||||
|
||||
List<String> get childrenIds => tagGroups['children'] ?? const [];
|
||||
|
||||
List<String> get poolIds => tagGroups['pools'] ?? const [];
|
||||
|
||||
bool get hasRelations =>
|
||||
(parentId != null && parentId!.isNotEmpty) || childrenIds.isNotEmpty;
|
||||
|
||||
bool get hasPools => poolIds.isNotEmpty;
|
||||
|
||||
int? get favCount {
|
||||
final raw = tagGroups['fav_count']?.firstOrNull;
|
||||
return raw != null ? int.tryParse(raw) : null;
|
||||
}
|
||||
|
||||
String? get title {
|
||||
final list = tagGroups['copyright'];
|
||||
if (list != null && list.isNotEmpty) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
|
||||
|
||||
import '../../core/http/dio_client.dart';
|
||||
import '../mappers/e621_mapper.dart';
|
||||
import '../models/e621_pool.dart';
|
||||
import '../models/post.dart';
|
||||
import '../models/post_comment.dart';
|
||||
import '../models/provider_health.dart';
|
||||
@@ -33,6 +34,24 @@ class E621Provider
|
||||
final Dio _dio;
|
||||
final Map<String, String> _queryParameters;
|
||||
|
||||
static DateTime _lastRequestTime = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
static Future<void> _throttle() async {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(_lastRequestTime);
|
||||
if (diff.inMilliseconds < 500) {
|
||||
final waitMs = 500 - diff.inMilliseconds;
|
||||
await Future<void>.delayed(Duration(milliseconds: waitMs));
|
||||
}
|
||||
_lastRequestTime = DateTime.now();
|
||||
}
|
||||
|
||||
bool get isAuthorized =>
|
||||
_queryParameters.containsKey('login') ||
|
||||
_dio.options.headers.containsKey('Authorization');
|
||||
|
||||
String? get login => _queryParameters['login'];
|
||||
|
||||
@override
|
||||
String postPageUrl(Post post) => '$baseUrl/posts/${post.id}';
|
||||
|
||||
@@ -44,6 +63,7 @@ class E621Provider
|
||||
String? rating,
|
||||
TopPeriodFilter topPeriod = TopPeriodFilter.none,
|
||||
}) async {
|
||||
await _throttle();
|
||||
final response = await _dio.get<dynamic>(
|
||||
'/posts.json',
|
||||
queryParameters: {
|
||||
@@ -66,6 +86,7 @@ class E621Provider
|
||||
|
||||
@override
|
||||
Future<Post?> getPost(String id) async {
|
||||
await _throttle();
|
||||
final response = await _dio.get<dynamic>(
|
||||
'/posts/$id.json',
|
||||
queryParameters: _queryParameters,
|
||||
@@ -78,6 +99,63 @@ class E621Provider
|
||||
return posts.isEmpty ? null : posts.first;
|
||||
}
|
||||
|
||||
Future<E621Pool?> getPool(String poolId) async {
|
||||
await _throttle();
|
||||
try {
|
||||
final response = await _dio.get<dynamic>(
|
||||
'/pools/$poolId.json',
|
||||
queryParameters: _queryParameters,
|
||||
);
|
||||
if (response.data is Map) {
|
||||
return E621Pool.fromJson(Map<String, dynamic>.from(response.data as Map));
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<bool> votePost(String postId, int score) async {
|
||||
await _throttle();
|
||||
try {
|
||||
final response = await _dio.post<dynamic>(
|
||||
'/posts/$postId/votes.json',
|
||||
data: {'score': score, 'no_unvote': 'true'},
|
||||
queryParameters: _queryParameters,
|
||||
options: Options(contentType: Headers.formUrlEncodedContentType),
|
||||
);
|
||||
return response.statusCode == 200 || response.statusCode == 201;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addFavorite(String postId) async {
|
||||
await _throttle();
|
||||
try {
|
||||
final response = await _dio.post<dynamic>(
|
||||
'/favorites.json',
|
||||
data: {'post_id': postId},
|
||||
queryParameters: _queryParameters,
|
||||
options: Options(contentType: Headers.formUrlEncodedContentType),
|
||||
);
|
||||
return response.statusCode == 200 || response.statusCode == 201;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> removeFavorite(String postId) async {
|
||||
await _throttle();
|
||||
try {
|
||||
final response = await _dio.delete<dynamic>(
|
||||
'/favorites/$postId.json',
|
||||
queryParameters: _queryParameters,
|
||||
);
|
||||
return response.statusCode == 200 || response.statusCode == 204;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ProviderHealth> checkHealth() async {
|
||||
final startedAt = DateTime.now();
|
||||
@@ -109,6 +187,7 @@ class E621Provider
|
||||
Future<List<TagSuggestion>> suggestTags(String query,
|
||||
{int limit = 20}) async {
|
||||
if (query.trim().isEmpty) return const [];
|
||||
await _throttle();
|
||||
final response = await _dio.get<dynamic>(
|
||||
'/tags.json',
|
||||
queryParameters: {
|
||||
@@ -136,6 +215,7 @@ class E621Provider
|
||||
|
||||
@override
|
||||
Future<List<PostComment>> getComments(String postId) async {
|
||||
await _throttle();
|
||||
final response = await _dio.get<dynamic>(
|
||||
'/comments.json',
|
||||
queryParameters: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../core/http/dio_client.dart';
|
||||
import '../models/content_provider_config.dart';
|
||||
import 'content_provider.dart';
|
||||
@@ -20,6 +22,17 @@ class ProviderFactory {
|
||||
if (entry.key.startsWith('query.') && entry.value.trim().isNotEmpty)
|
||||
entry.key.substring('query.'.length): entry.value.trim(),
|
||||
};
|
||||
|
||||
if (config.apiType.toLowerCase() == 'e621') {
|
||||
final login = queryParameters['login'] ?? headers['login'];
|
||||
final apiKey = queryParameters['api_key'] ?? headers['api_key'];
|
||||
if (login != null && apiKey != null && login.isNotEmpty && apiKey.isNotEmpty) {
|
||||
final basicAuth = base64Encode(utf8.encode('$login:$apiKey'));
|
||||
headers['Authorization'] = 'Basic $basicAuth';
|
||||
headers['User-Agent'] = 'Prisma/3.6.6 (by $login on e621)';
|
||||
}
|
||||
}
|
||||
|
||||
final client = DioClient(
|
||||
baseUrl: config.baseUrl,
|
||||
timeout: Duration(seconds: config.timeoutSeconds),
|
||||
|
||||
@@ -144,6 +144,14 @@ class ProviderManager {
|
||||
});
|
||||
}
|
||||
|
||||
Future<ContentProvider?> getProviderInstance(String id) async {
|
||||
final result = await _repository.getProvider(id);
|
||||
if (result is Success<ContentProviderConfig?> && result.data != null) {
|
||||
return _factory.create(result.data!);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Result<List<ProviderHealth>>> checkAll({int concurrency = 3}) async {
|
||||
final providersResult = await activeProviders();
|
||||
if (providersResult is Error<List<ContentProvider>>) {
|
||||
|
||||
Reference in New Issue
Block a user