diff --git a/README.md b/README.md
index 16b3ba2..099b584 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
diff --git a/README_RU.md b/README_RU.md
index 7de87ae..064980b 100644
--- a/README_RU.md
+++ b/README_RU.md
@@ -14,7 +14,7 @@
-
+
diff --git a/lib/app/app_version.dart b/lib/app/app_version.dart
index 09c7d43..da6cdf2 100644
--- a/lib/app/app_version.dart
+++ b/lib/app/app_version.dart
@@ -1,3 +1,3 @@
-const appDisplayVersion = '3.5.3';
-const appBuildNumber = 79;
+const appDisplayVersion = '3.5.4';
+const appBuildNumber = 80;
const appReleaseChannel = 'Stable';
diff --git a/lib/app/changelog.dart b/lib/app/changelog.dart
index cc69d6c..ee65aff 100644
--- a/lib/app/changelog.dart
+++ b/lib/app/changelog.dart
@@ -1,4 +1,13 @@
const prismaChangelog = [
+ PrismaChange(
+ version: '3.5.4',
+ title: 'Исправление распознавания MP4-видео и отключение устаревших сервисов Kemono/Coomer',
+ bullets: [
+ 'Устранена ошибка классификации видео: файлы MP4, содержащие слово «audio» в имени файла или параметрах запроса, больше не распознаются как аудио и корректно воспроизводятся как полноценное видео.',
+ 'Точное определение расширений: внедрён строгий парсинг расширений файлов, исключающий ложные срабатывания при определении типа медиа.',
+ 'Удаление устаревших провайдеров Kemono и Coomer: окончательно удалены неработающие сервисы Kemono и Coomer из списка провайдеров и настроек (основным источником артистов является Pawchive).',
+ ],
+ ),
PrismaChange(
version: '3.5.3',
title: 'Поддержка аудиозаписей (MP3/WAV/FLAC) и исправление загрузки медиа у авторов',
diff --git a/lib/backend/providers/kemono_provider.dart b/lib/backend/providers/kemono_provider.dart
deleted file mode 100644
index 5160d9d..0000000
--- a/lib/backend/providers/kemono_provider.dart
+++ /dev/null
@@ -1,658 +0,0 @@
-import 'package:dio/dio.dart';
-
-import '../../core/http/dio_client.dart';
-import '../models/artist_announcement.dart';
-import '../models/artist_link.dart';
-import '../models/artist_profile.dart';
-import '../models/artist_tag.dart';
-import '../models/artist_work_query.dart';
-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
- implements ContentProvider, ArtistProvider, CommentProvider {
- KemonoProvider({
- required this.id,
- required this.name,
- required this.baseUrl,
- required DioClient dioClient,
- Map queryParameters = const {},
- }) : _dio = dioClient.dio,
- _creatorDio = Dio(
- BaseOptions(
- baseUrl: queryParameters['creator_api_base'] ??
- 'https://kemono-api.mbaharip.com',
- connectTimeout: const Duration(seconds: 15),
- receiveTimeout: const Duration(seconds: 20),
- headers: const {
- 'Accept': 'application/json',
- 'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
- },
- ),
- );
-
- @override
- final String id;
-
- @override
- final String name;
-
- @override
- final String baseUrl;
-
- final Dio _dio;
- final Dio _creatorDio;
-
- bool get _isCoomer =>
- id.toLowerCase().contains('coomer') ||
- baseUrl.toLowerCase().contains('coomer');
-
- @override
- Future> searchPosts({
- required List tags,
- required int page,
- int limit = 50,
- String? rating,
- TopPeriodFilter topPeriod = TopPeriodFilter.none,
- }) async {
- return const [];
- }
-
- @override
- Future getPost(String id) async => null;
-
- @override
- Future checkHealth() async {
- final startedAt = DateTime.now();
- try {
- await _creatorDio.get(
- _creatorPath(),
- queryParameters: {'page': 1, 'itemsPerPage': 1},
- );
- return ProviderHealth(
- providerId: id,
- status: ProviderStatus.online,
- pingMs: DateTime.now().difference(startedAt).inMilliseconds,
- lastCheckedAt: DateTime.now(),
- apiVersion: _isCoomer ? 'coomer-artists' : 'kemono-artists',
- );
- } catch (error) {
- return ProviderHealth(
- providerId: id,
- status: ProviderStatus.offline,
- pingMs: DateTime.now().difference(startedAt).inMilliseconds,
- lastCheckedAt: DateTime.now(),
- errorMessage: error.toString(),
- apiVersion: _isCoomer ? 'coomer-artists' : 'kemono-artists',
- );
- }
- }
-
- @override
- Future> listArtists({
- String? service,
- List? services,
- String? query,
- int page = 1,
- int limit = 30,
- }) async {
- final effectiveService =
- services != null && services.length == 1 ? services.first : service;
- List list;
- try {
- final response = await _creatorDio.get(
- effectiveService == null || effectiveService.isEmpty
- ? _creatorPath()
- : '${_creatorPath()}/$effectiveService',
- queryParameters: {
- 'page': page,
- 'itemsPerPage': limit.clamp(1, 100),
- if (query != null && query.trim().isNotEmpty) 'keyword': query.trim(),
- },
- );
- list = _rankArtists(_artistsFromResponse(response.data), query ?? '');
- } catch (_) {
- list = await _listArtistsFromPublicApi(
- service: effectiveService,
- query: query,
- page: page,
- limit: limit,
- );
- }
- if (services != null && services.isNotEmpty) {
- final sSet = services.map((s) => s.toLowerCase()).toSet();
- list = list.where((a) => sSet.contains(a.service.toLowerCase())).toList();
- }
- return list;
- }
-
- @override
- Future> searchArtists(
- String query, {
- String? service,
- List? services,
- int page = 1,
- int limit = 30,
- }) {
- return listArtists(
- service: service,
- services: services,
- query: query,
- page: page,
- limit: limit,
- );
- }
-
- @override
- Future> getArtistPosts({
- required ArtistWorkQuery query,
- int page = 0,
- int limit = 50,
- }) async {
- Object? lastError;
- final paths = [
- '/api/v1/${query.service}/user/${query.artistId}/posts',
- '/api/v1/${query.service}/user/${query.artistId}',
- ];
- for (final apiBase in _apiBaseCandidates()) {
- for (final path in paths) {
- try {
- final response = await _getFromApiBase(
- apiBase,
- path,
- queryParameters: {'o': page * limit},
- );
- final posts = _postItems(response.data);
- final mapped = [];
- for (final post in posts.whereType