feat: add audio playback support (MP3/WAV/FLAC) and fix attachment loading in creator posts
Build and Release / Build Windows x64 (push) Has been cancelled
Build and Release / Build macOS (push) Has been cancelled

This commit is contained in:
2026-09-05 15:16:08 +03:00
parent 2dad5e9c82
commit 5e3efb0da9
13 changed files with 1073 additions and 54 deletions
+38 -5
View File
@@ -600,14 +600,47 @@ class KemonoProvider
}
String _fileType(String value) {
final lower = value.toLowerCase().split('?').first;
if (lower.endsWith('.webm') || lower.endsWith('.mp4')) return 'video';
final lower = value.toLowerCase().split('?').first.trim();
if (lower.endsWith('.webm') ||
lower.endsWith('.mp4') ||
lower.endsWith('.mov') ||
lower.endsWith('.mkv') ||
lower.endsWith('.avi') ||
lower.endsWith('.flv') ||
lower.endsWith('.wmv') ||
lower.endsWith('.m4v') ||
lower.endsWith('.ts')) {
return 'video';
}
if (lower.endsWith('.mp3') ||
lower.endsWith('.m4a') ||
lower.endsWith('.wav') ||
lower.endsWith('.ogg') ||
lower.endsWith('.flac') ||
lower.endsWith('.aac') ||
lower.endsWith('.opus') ||
lower.endsWith('.wma')) {
return 'audio';
}
if (lower.endsWith('.gif')) return 'gif';
if (lower.endsWith('.swf')) return 'swf';
if (lower.endsWith('.png')) return 'png';
if (lower.endsWith('.jpg') ||
if (lower.endsWith('.zip') ||
lower.endsWith('.rar') ||
lower.endsWith('.7z') ||
lower.endsWith('.tar') ||
lower.endsWith('.gz') ||
lower.endsWith('.pdf') ||
lower.endsWith('.txt')) {
return 'archive';
}
if (lower.endsWith('.png') ||
lower.endsWith('.jpg') ||
lower.endsWith('.jpeg') ||
lower.endsWith('.webp')) {
lower.endsWith('.webp') ||
lower.endsWith('.avif') ||
lower.endsWith('.bmp') ||
lower.endsWith('.heic') ||
lower.endsWith('.tiff')) {
return 'photo';
}
return 'unknown';
+68 -8
View File
@@ -414,7 +414,15 @@ class PawchiveProvider
}
final attachments = json['attachments'];
if (attachments is List) {
files.addAll(attachments.whereType<Map>().map(Map<String, dynamic>.from));
for (final att in attachments.whereType<Map>()) {
final m = Map<String, dynamic>.from(att);
final path = (m['path'] ?? '').toString();
if (path.isNotEmpty &&
files.any((existing) => (existing['path'] ?? '').toString() == path)) {
continue;
}
files.add(m);
}
}
// Support link-only posts where author uploaded files to cloud drives (MEGA, GDrive, etc.)
@@ -468,6 +476,17 @@ class PawchiveProvider
];
}
// Determine if there is a cover illustration for the post
String? postCoverUrl;
for (final f in files) {
final p = (f['path'] ?? '').toString();
final n = (f['name'] ?? '').toString();
if (_isPhotoType('$n $p') && p.isNotEmpty) {
postCoverUrl = _thumbnailUrl(p);
break;
}
}
var index = 0;
return files
.map((fileMap) {
@@ -475,18 +494,26 @@ class PawchiveProvider
final fileName = (fileMap['name'] ?? '').toString();
final previewOnly = fileMap['preview_only'] == true;
final previewUrl = _thumbnailUrl(rawPath);
final type = _fileType('$fileName $rawPath');
final isPhoto = _isPhotoType('$fileName $rawPath');
// img.pawchive.pw/thumbnail only generates thumbnails for images/gifs.
// Non-image files (audio, video, archive) return 404 from thumbnail server.
final previewUrl = isPhoto
? _thumbnailUrl(rawPath)
: (postCoverUrl ?? '');
final fileUrl = previewOnly
? previewUrl
? (isPhoto ? previewUrl : _fileDownloadUrl(rawPath, fileName))
: _fileDownloadUrl(rawPath, fileName);
final type = _fileType('$fileName $rawPath');
final stableId =
'${query.service}:${query.artistId}:$postId:${index++}';
final tags = [
query.service,
query.artistName,
if (title.trim().isNotEmpty) title.trim(),
if (type == 'audio') 'audio',
];
return Post(
@@ -600,24 +627,57 @@ class PawchiveProvider
}
String _fileType(String value) {
final lower = value.toLowerCase().split('?').first;
if (lower.endsWith('.webm') || lower.endsWith('.mp4') || lower.endsWith('.mov')) {
final lower = value.toLowerCase().split('?').first.trim();
if (lower.endsWith('.webm') ||
lower.endsWith('.mp4') ||
lower.endsWith('.mov') ||
lower.endsWith('.mkv') ||
lower.endsWith('.avi') ||
lower.endsWith('.flv') ||
lower.endsWith('.wmv') ||
lower.endsWith('.m4v') ||
lower.endsWith('.ts')) {
return 'video';
}
if (lower.endsWith('.mp3') ||
lower.endsWith('.m4a') ||
lower.endsWith('.wav') ||
lower.endsWith('.ogg') ||
lower.endsWith('.flac') ||
lower.endsWith('.aac') ||
lower.endsWith('.opus') ||
lower.endsWith('.wma')) {
return 'audio';
}
if (lower.endsWith('.gif')) return 'gif';
if (lower.endsWith('.swf')) return 'swf';
if (lower.endsWith('.zip') || lower.endsWith('.rar') || lower.endsWith('.7z')) {
if (lower.endsWith('.zip') ||
lower.endsWith('.rar') ||
lower.endsWith('.7z') ||
lower.endsWith('.tar') ||
lower.endsWith('.gz') ||
lower.endsWith('.pdf') ||
lower.endsWith('.txt')) {
return 'archive';
}
if (lower.endsWith('.png') ||
lower.endsWith('.jpg') ||
lower.endsWith('.jpeg') ||
lower.endsWith('.webp')) {
lower.endsWith('.webp') ||
lower.endsWith('.avif') ||
lower.endsWith('.bmp') ||
lower.endsWith('.heic') ||
lower.endsWith('.tiff')) {
return 'photo';
}
return 'unknown';
}
bool _isPhotoType(String value) {
final type = _fileType(value);
return type == 'photo' || type == 'gif';
}
DateTime? _dateFromAny(dynamic value) {
if (value == null) return null;
if (value is num) {
+49 -2
View File
@@ -80,7 +80,16 @@ class MediaUrlSelector {
]);
}
static List<String> audio(Post post) {
return _compact([
post.fileUrl,
if (_looksLikeAudioUrl(post.sampleUrl)) post.sampleUrl,
if (_looksLikeAudioUrl(post.previewUrl)) post.previewUrl,
]);
}
static bool isVideo(Post post) => _isVideo(post);
static bool isAudio(Post post) => _isAudio(post);
static bool isGif(Post post) => _isGif(post);
static List<String> _compact(Iterable<String> urls) {
@@ -91,12 +100,32 @@ class MediaUrlSelector {
.toList(growable: false);
}
static bool _isAudio(Post post) {
final value = '${post.fileType} ${post.fileUrl}'.toLowerCase();
return value.contains('audio') ||
value.contains('.mp3') ||
value.contains('.m4a') ||
value.contains('.wav') ||
value.contains('.ogg') ||
value.contains('.flac') ||
value.contains('.aac') ||
value.contains('.opus') ||
value.contains('.wma');
}
static bool _isVideo(Post post) {
if (_isAudio(post)) return false;
final value = '${post.fileType} ${post.fileUrl}'.toLowerCase();
return value.contains('video') ||
value.contains('.webm') ||
value.contains('.mp4') ||
value.contains('.mov');
value.contains('.mov') ||
value.contains('.mkv') ||
value.contains('.avi') ||
value.contains('.flv') ||
value.contains('.wmv') ||
value.contains('.m4v') ||
value.contains('.ts');
}
static bool _isGif(Post post) {
@@ -108,6 +137,24 @@ class MediaUrlSelector {
final value = url.toLowerCase();
return value.contains('.webm') ||
value.contains('.mp4') ||
value.contains('.mov');
value.contains('.mov') ||
value.contains('.mkv') ||
value.contains('.avi') ||
value.contains('.flv') ||
value.contains('.wmv') ||
value.contains('.m4v') ||
value.contains('.ts');
}
static bool _looksLikeAudioUrl(String url) {
final value = url.toLowerCase();
return value.contains('.mp3') ||
value.contains('.m4a') ||
value.contains('.wav') ||
value.contains('.ogg') ||
value.contains('.flac') ||
value.contains('.aac') ||
value.contains('.opus') ||
value.contains('.wma');
}
}