feat: connect file actions and media recognition

This commit is contained in:
ngfchl
2026-07-18 11:16:02 +08:00
parent 676f5057cb
commit c37fbe9e7f
4 changed files with 207 additions and 25 deletions
+70 -6
View File
@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:url_launcher/url_launcher.dart';
import '../api/guangya_api.dart';
import '../models/cloud_file.dart';
@@ -430,20 +432,82 @@ class FileNotifier extends StateNotifier<FileState> {
Future<void> downloadFile(CloudFile file) async {
if (_api == null) return;
try {
final result = await _api!.downloadURL(file.id);
final url = _findStringDeep(result, [
state = state.copyWith(statusMessage: '正在准备外部打开…');
final url = await _resolveOpenUrl(file);
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
throw Exception('无法调用系统默认应用打开链接');
}
state = state.copyWith(statusMessage: '已交给系统默认应用');
} catch (e) {
state = state.copyWith(errorMessage: e.toString());
}
}
Future<void> createShare(CloudFile file) async {
if (_api == null) return;
try {
state = state.copyWith(statusMessage: '正在创建分享…');
final result = await _api!.shareCreate([file.id], title: file.name);
final link = _findStringDeep(result, const [
'url',
'downloadUrl',
'download_url',
'shareUrl',
'share_url',
'link',
]);
if (url != null) {
state = state.copyWith(statusMessage: '下载链接: $url');
if (link != null) {
await Clipboard.setData(ClipboardData(text: link));
state = state.copyWith(statusMessage: '分享链接已复制');
} else {
state = state.copyWith(statusMessage: '分享已创建');
}
} catch (e) {
state = state.copyWith(errorMessage: e.toString());
}
}
Future<void> restoreFiles(List<CloudFile> files) async {
if (_api == null || files.isEmpty) return;
try {
state = state.copyWith(statusMessage: '正在恢复 ${files.length} 个项目…');
await _api!.fsRecycle(files.map((file) => file.id).toList());
state = state.copyWith(statusMessage: '已恢复 ${files.length} 个项目');
await loadFiles(parentID: _currentParentID);
} catch (e) {
state = state.copyWith(errorMessage: e.toString());
}
}
Future<Uri> _resolveOpenUrl(CloudFile file) async {
String? url;
if (file.isVideo) {
final detail = await _api!.fsDetail(file.id);
final gcid = file.gcid ?? _findStringDeep(detail, const ['gcid', 'gcId']);
if (gcid != null && gcid.isNotEmpty) {
final videoResult = await _api!.vodDownloadURL(file.id, gcid);
url = _findStringDeep(videoResult, const [
'signedURL',
'signedUrl',
'url',
'downloadUrl',
'download_url',
'dlink',
]);
}
}
if (url == null) {
final result = await _api!.downloadURL(file.id);
url = _findStringDeep(result, const [
'url',
'downloadUrl',
'download_url',
'dlink',
]);
}
final uri = url == null ? null : Uri.tryParse(url);
if (uri == null || !uri.hasScheme) throw Exception('响应缺少可打开的签名链接');
return uri;
}
Future<void> uploadLocalFiles(List<File> files, {String? parentID}) async {
if (_api == null || files.isEmpty) return;
final targetParentID = parentID ?? _currentParentID;
+69 -1
View File
@@ -203,8 +203,19 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
}
final unique = <String, MediaLibraryItem>{};
final tmdbApiKey =
StorageManager.get<String>(StorageKeys.tmdbApiKey) ?? '';
for (final file in discovered) {
unique[file.id] = MediaLibraryItem.fromFile(library.id, file);
if (_cancelScan) break;
final fallback = MediaLibraryItem.fromFile(library.id, file);
unique[file.id] = await _recognizeMediaItem(fallback, tmdbApiKey);
state = state.copyWith(
progress: MediaLibraryScanProgress(
phase: tmdbApiKey.isEmpty ? '正在建立本地索引' : '正在识别 ${file.name}',
completed: unique.length,
total: discovered.length,
),
);
}
final items = unique.values.toList()
..sort(
@@ -264,6 +275,63 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
state = state.copyWith(clearError: true);
}
Future<MediaLibraryItem> _recognizeMediaItem(
MediaLibraryItem fallback,
String apiKey,
) async {
if (_api == null || apiKey.trim().isEmpty) return fallback;
try {
final result = await _api!.tmdbSearch(fallback.title, apiKey: apiKey);
final values = result['results'];
if (values is! List) return fallback;
Map<String, dynamic>? candidate;
for (final value in values) {
if (value is! Map) continue;
final map = Map<String, dynamic>.from(value);
final type = map['media_type']?.toString();
if (type == 'movie' || type == 'tv') {
candidate = map;
break;
}
}
if (candidate == null) return fallback;
final type = candidate['media_type']?.toString();
final title = (candidate['title'] ?? candidate['name'])
?.toString()
.trim();
final originalTitle =
(candidate['original_title'] ?? candidate['original_name'])
?.toString()
.trim();
final releaseDate =
(candidate['release_date'] ?? candidate['first_air_date'])
?.toString() ??
'';
return MediaLibraryItem(
libraryID: fallback.libraryID,
file: fallback.file,
tmdbID: _toInt(candidate['id']),
title: title == null || title.isEmpty ? fallback.title : title,
originalTitle: originalTitle == null || originalTitle.isEmpty
? fallback.originalTitle
: originalTitle,
mediaKind: type == 'tv' ? TMDBMediaKind.tv : TMDBMediaKind.movie,
releaseDate: releaseDate,
overview: candidate['overview']?.toString() ?? '',
posterPath: candidate['poster_path']?.toString(),
backdropPath: candidate['backdrop_path']?.toString(),
updatedAt: DateTime.now(),
);
} catch (_) {
return fallback;
}
}
int? _toInt(dynamic value) {
if (value is int) return value;
return int.tryParse(value?.toString() ?? '');
}
Future<List<CloudFile>> _scanSource(
String? rootID,
String rootPath, {