feat: artist cloud drives & external video mirrors (v2.3.0)

This commit is contained in:
2026-09-04 12:08:01 +03:00
parent f603c37c91
commit 639bfc50af
16 changed files with 1369 additions and 10 deletions
+57 -2
View File
@@ -10,6 +10,7 @@ import '../models/post.dart';
import '../models/post_comment.dart';
import '../models/provider_health.dart';
import '../models/top_period_filter.dart';
import '../services/cloud_link_extractor.dart';
import 'content_provider.dart';
class KemonoProvider
@@ -361,6 +362,18 @@ class KemonoProvider
_dateFromAny(json['added']) ??
_dateFromAny(json['edited']) ??
DateTime.fromMillisecondsSinceEpoch(0);
final postSource =
'$baseUrl/${query.service}/user/${query.artistId}/post/$postId';
final rawContent = (json['content'] ?? '').toString();
final embed = json['embed'] is Map ? Map<String, dynamic>.from(json['embed']) : null;
final cloudLinks = CloudLinkExtractor.extractLinks(
content: rawContent,
source: postSource,
embed: embed,
);
final cloudLinksJson = cloudLinks.map((e) => e.encode()).toList(growable: false);
final cleanDesc = CloudLinkExtractor.cleanCommentary(rawContent);
final files = <Map<String, dynamic>>[];
final file = json['file'];
if (file is Map && file.isNotEmpty) {
@@ -370,6 +383,47 @@ class KemonoProvider
if (attachments is List) {
files.addAll(attachments.whereType<Map>().map(Map<String, dynamic>.from));
}
// Support link-only posts where author uploaded files to cloud drives (MEGA, GDrive, etc.)
if (files.isEmpty && (cloudLinks.isNotEmpty || cleanDesc.isNotEmpty)) {
final stableId = '${query.service}:${query.artistId}:$postId:0';
final firstStreamable =
cloudLinks.where((l) => l.isStreamable).firstOrNull;
final firstLink = cloudLinks.isNotEmpty ? cloudLinks.first : null;
final fileUrl = firstStreamable?.directStreamUrl ?? firstLink?.url ?? '';
final tags = [
query.service,
query.artistName,
if (title.trim().isNotEmpty) title.trim(),
'cloud_mirror',
];
return [
Post(
id: stableId,
providerId: id,
providerName: name,
previewUrl: '',
sampleUrl: '',
fileUrl: fileUrl,
tags: tags,
rating: 'unknown',
width: 0,
height: 0,
source: postSource,
createdAt: published,
fileType: firstStreamable != null ? 'video' : 'link',
score: 0,
tagGroups: {
'artist': [query.artistName],
'meta': [query.service],
if (title.trim().isNotEmpty) 'copyright': [title.trim()],
if (cloudLinksJson.isNotEmpty) 'cloud_links': cloudLinksJson,
if (cleanDesc.isNotEmpty) 'description': [cleanDesc],
},
),
];
}
var index = 0;
return files
.map((file) {
@@ -396,8 +450,7 @@ class KemonoProvider
rating: 'unknown',
width: 0,
height: 0,
source:
'$baseUrl/${query.service}/user/${query.artistId}/post/$postId',
source: postSource,
createdAt: published,
fileType: type,
score: 0,
@@ -405,6 +458,8 @@ class KemonoProvider
'artist': [query.artistName],
'meta': [query.service],
if (title.trim().isNotEmpty) 'copyright': [title.trim()],
if (cloudLinksJson.isNotEmpty) 'cloud_links': cloudLinksJson,
if (cleanDesc.isNotEmpty) 'description': [cleanDesc],
},
);
})
+56 -2
View File
@@ -12,6 +12,7 @@ import '../models/post.dart';
import '../models/post_comment.dart';
import '../models/provider_health.dart';
import '../models/top_period_filter.dart';
import '../services/cloud_link_extractor.dart';
import 'content_provider.dart';
class PawchiveProvider
@@ -394,6 +395,18 @@ class PawchiveProvider
_dateFromAny(json['added']) ??
_dateFromAny(json['edited']) ??
DateTime.fromMillisecondsSinceEpoch(0);
final postSource =
'$baseUrl/${query.service}/user/${query.artistId}/post/$postId';
final rawContent = (json['content'] ?? '').toString();
final embed = json['embed'] is Map ? Map<String, dynamic>.from(json['embed']) : null;
final cloudLinks = CloudLinkExtractor.extractLinks(
content: rawContent,
source: postSource,
embed: embed,
);
final cloudLinksJson = cloudLinks.map((e) => e.encode()).toList(growable: false);
final cleanDesc = CloudLinkExtractor.cleanCommentary(rawContent);
final files = <Map<String, dynamic>>[];
final file = json['file'];
if (file is Map && file.isNotEmpty) {
@@ -404,6 +417,46 @@ class PawchiveProvider
files.addAll(attachments.whereType<Map>().map(Map<String, dynamic>.from));
}
// Support link-only posts where author uploaded files to cloud drives (MEGA, GDrive, etc.)
if (files.isEmpty && (cloudLinks.isNotEmpty || cleanDesc.isNotEmpty)) {
final stableId = '${query.service}:${query.artistId}:$postId:0';
final firstStreamable =
cloudLinks.where((l) => l.isStreamable).firstOrNull;
final firstLink = cloudLinks.isNotEmpty ? cloudLinks.first : null;
final fileUrl = firstStreamable?.directStreamUrl ?? firstLink?.url ?? '';
final tags = [
query.service,
query.artistName,
if (title.trim().isNotEmpty) title.trim(),
'cloud_mirror',
];
return [
Post(
id: stableId,
providerId: id,
providerName: name,
previewUrl: '',
sampleUrl: '',
fileUrl: fileUrl,
tags: tags,
rating: 'unknown',
width: 0,
height: 0,
source: postSource,
createdAt: published,
fileType: firstStreamable != null ? 'video' : 'link',
score: 0,
tagGroups: {
'artist': [query.artistName],
'meta': [query.service],
if (title.trim().isNotEmpty) 'copyright': [title.trim()],
if (cloudLinksJson.isNotEmpty) 'cloud_links': cloudLinksJson,
if (cleanDesc.isNotEmpty) 'description': [cleanDesc],
},
),
];
}
var index = 0;
return files
.map((fileMap) {
@@ -436,8 +489,7 @@ class PawchiveProvider
rating: 'unknown',
width: 0,
height: 0,
source:
'$baseUrl/${query.service}/user/${query.artistId}/post/$postId',
source: postSource,
createdAt: published,
fileType: type,
score: 0,
@@ -445,6 +497,8 @@ class PawchiveProvider
'artist': [query.artistName],
'meta': [query.service],
if (title.trim().isNotEmpty) 'copyright': [title.trim()],
if (cloudLinksJson.isNotEmpty) 'cloud_links': cloudLinksJson,
if (cleanDesc.isNotEmpty) 'description': [cleanDesc],
},
);
})