Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Documentation/torrent-download.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ NipaPlay 的下载器可以管理种子文件和磁力链接任务,并在任
1. 打开下载器,选择添加种子文件或磁力链接。
2. 选择下载目录;移动设备需先确认系统存储权限。
3. 如需按任务分类,启用“为任务创建目录”。
4. 查看元数据和文件列表后开始任务;可暂停、继续或移除任务。
5. 删除任务前确认是否同时删除本地数据。该操作可能不可恢复。
4. 预览元数据和文件列表后添加任务;添加直接复用预览结果。解析最多等待 60 秒,可取消或修改链接后重试。
5. 下载中可选择视频边下边播;初始化暂停后可继续,错误任务可重试,完成后的做种也可暂停。
6. 删除任务前确认是否同时删除本地数据。该操作可能不可恢复。

## 使用建议

Expand All @@ -20,3 +21,10 @@ NipaPlay 的下载器可以管理种子文件和磁力链接任务,并在任
## 排障信息

报告问题时附上任务状态、保存目录是否可写、可用磁盘空间、平台、NipaPlay 版本和脱敏日志;不要公开含有私人 tracker、token 或个人路径的 magnet/日志。

## 任务状态与保存

- 添加过程中显示处理提示;操作完成后立即刷新对应状态,其他任务仍可操作。
- 重复添加会提示任务已存在,并保留原任务的状态和保存位置。继续下载请使用任务的继续或重试按钮。
- Android 和鸿蒙将任务记录保存在应用支持目录,不随下载位置改变。升级时会从当前下载目录、最近使用的目录和默认下载目录导入旧记录;保留原文件、暂停状态和旧记录备份。
- 自动入库只扫描该任务已经完成的视频文件。失败会重试,不会把共享下载目录中的其他未完成文件一并扫描。旧版本的扫描完成标记会重新验证一次。
1 change: 1 addition & 0 deletions lib/constants/settings_keys.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class SettingsKeys {
static const String downloaderAutoScanCompletedTasks =
'downloader_auto_scan_completed_tasks';

static const String downloaderVerifiedScannedCompletedTaskKeys = 'downloader_verified_scanned_completed_task_keys';
static const String downloaderAutoScannedCompletedTaskKeys =
'downloader_auto_scanned_completed_task_keys';

Expand Down
54 changes: 54 additions & 0 deletions lib/downloads/torrent_task_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:flutter/foundation.dart';
import 'package:nipaplay/models/torrent_task.dart';

/// Serializes snapshots and locks individual tasks until their changed state has
/// been read back. A slow add or another task never blocks list polling.
class TorrentTaskController extends ChangeNotifier {
TorrentTaskController({required this.loadTasks});

final Future<List<TorrentTask>> Function() loadTasks;
List<TorrentTask> _tasks = const [];
List<TorrentTask> get tasks => _tasks;
final Set<int> _busyIds = {};
bool isBusy(int id) => _busyIds.contains(id);
Future<void>? _refresh;
bool _refreshAgain = false;
bool _disposed = false;

Future<void> refresh() {
if (_disposed) return Future<void>.value();
_refreshAgain = true;
return _refresh ??= _drainRefresh().whenComplete(() => _refresh = null);
}

Future<void> _drainRefresh() async {
do {
_refreshAgain = false;
final tasks = await loadTasks();
if (_disposed) return;
_tasks = List.unmodifiable(tasks);
notifyListeners();
} while (_refreshAgain);
}

Future<bool> run(int id, Future<void> Function() action) async {
if (_disposed || !_busyIds.add(id)) return false;
notifyListeners();
try {
await action();
// If a pre-action poll was already in flight, refresh() schedules another
// read before releasing this task's controls.
await refresh();
return true;
} finally {
_busyIds.remove(id);
if (!_disposed) notifyListeners();
}
}

@override
void dispose() {
_disposed = true;
super.dispose();
}
}
14 changes: 10 additions & 4 deletions lib/downloads/unified_torrent_page_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:nipaplay/models/torrent_task.dart';
import 'package:nipaplay/models/torrent_task_scan_summary.dart';
import 'package:flutter/widgets.dart';
import 'package:nipaplay/models/torrent_magnet_preview.dart';
import 'package:nipaplay/services/torrent_download_service.dart';

enum UnifiedTorrentTaskViewMode { cards, list }

Expand All @@ -19,7 +20,7 @@ class UnifiedTorrentTaskActionViewModel {

final UnifiedTorrentTaskAction action;
final String label;
final VoidCallback onPressed;
final VoidCallback? onPressed;
final bool destructive;
}

Expand Down Expand Up @@ -47,9 +48,10 @@ class UnifiedTorrentTaskItemViewModel {
return null;
}

UnifiedTorrentTaskActionViewModel get primaryAction => task.finished
? action(UnifiedTorrentTaskAction.play)!
: action(UnifiedTorrentTaskAction.toggle)!;
UnifiedTorrentTaskActionViewModel get primaryAction =>
task.finished && task.canPlay
? action(UnifiedTorrentTaskAction.play)!
: action(UnifiedTorrentTaskAction.toggle)!;

String? get scanStatusText {
if (isAutoScanning) return '正在加入媒体库...';
Expand Down Expand Up @@ -134,8 +136,12 @@ class AddTorrentDialogResult {
required this.magnetUri,
required this.downloadDirectory,
required this.createFolderForTask,
required this.preview,
required this.previewRequest,
});

final TorrentPreviewRequest previewRequest;
final TorrentMagnetPreview preview;
final String magnetUri;
final String downloadDirectory;
final bool createFolderForTask;
Expand Down
24 changes: 24 additions & 0 deletions lib/models/torrent_add_result.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'dart:convert';

class TorrentAddResult {
const TorrentAddResult(
{required this.id,
required this.alreadyExists,
required this.outputFolder});

final int id;
final bool alreadyExists;
final String outputFolder;

factory TorrentAddResult.fromJson(String json) {
final data = jsonDecode(json) as Map<String, dynamic>;
return TorrentAddResult(
id: (data['id'] as num).toInt(),
alreadyExists: data['already_exists'] == true,
outputFolder: data['output_folder'] as String,
);
}

String get message =>
alreadyExists ? '任务已存在,保留原状态和保存位置:$outputFolder' : '已添加下载任务';
}
5 changes: 4 additions & 1 deletion lib/models/torrent_magnet_preview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ class TorrentMagnetPreview {
required this.suggestedFolderName,
required this.totalSize,
required this.files,
this.requestId = 0,
});

final int requestId;
final String name;
final String suggestedFolderName;
final int totalSize;
final List<TorrentMagnetPreviewFile> files;

factory TorrentMagnetPreview.fromJson(String jsonText) {
factory TorrentMagnetPreview.fromJson(String jsonText, {int requestId = 0}) {
final decoded = jsonDecode(jsonText);
if (decoded is! Map<String, dynamic>) {
return const TorrentMagnetPreview(
Expand All @@ -50,6 +52,7 @@ class TorrentMagnetPreview {

final rawFiles = decoded['files'];
return TorrentMagnetPreview(
requestId: requestId,
name: _asString(decoded['name'], fallback: '未命名任务'),
suggestedFolderName: _asString(decoded['suggested_folder_name']),
totalSize: _asInt(decoded['total_size']),
Expand Down
26 changes: 23 additions & 3 deletions lib/models/torrent_task.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class TorrentTask {
required this.uploadSpeedBytesPerSecond,
required this.error,
this.files = const <TorrentTaskFile>[],
this.initializingPaused = false,
});

final int id;
Expand All @@ -85,6 +86,7 @@ class TorrentTask {
final int uploadedBytes;
final int totalBytes;
final bool finished;
final bool initializingPaused;
final int downloadSpeedBytesPerSecond;
final int uploadSpeedBytesPerSecond;
final String? error;
Expand All @@ -95,17 +97,33 @@ class TorrentTask {
return (progressBytes / totalBytes).clamp(0.0, 1.0);
}

bool get isPaused => state == 'paused';
bool get isPaused =>
state == 'paused' || (state == 'initializing' && initializingPaused);

bool get isActive => state == 'live' || state == 'initializing';
bool get canResume => isPaused || hasError;

bool get canPlay => !hasError && state != 'initializing';

String get toggleLabel => hasError
? '重试'
: isPaused
? (finished ? '继续做种' : '继续下载')
: (finished ? '暂停做种' : '暂停下载');

bool get isActive =>
!finished &&
!isPaused &&
!hasError &&
(state == 'live' || state == 'initializing');

bool get hasError => state == 'error' || (error?.isNotEmpty ?? false);

String get autoScanKey => '$infoHash|$outputFolder';

String get displayState {
if (hasError) return '错误';
if (finished) return '已完成';
if (finished) return isPaused ? '已完成(已暂停)' : '已完成(做种中)';
if (isPaused) return '已暂停';
switch (state) {
case 'initializing':
return '初始化';
Expand Down Expand Up @@ -136,6 +154,7 @@ class TorrentTask {
uploadedBytes: _asInt(stats['uploaded_bytes']),
totalBytes: _asInt(stats['total_bytes']),
finished: stats['finished'] == true,
initializingPaused: stats['initializing_paused'] == true,
downloadSpeedBytesPerSecond:
_speedBytes(live['download_speed'] as Map<String, dynamic>?),
uploadSpeedBytesPerSecond:
Expand All @@ -158,6 +177,7 @@ class TorrentTask {
uploadedBytes: uploadedBytes,
totalBytes: totalBytes,
finished: finished,
initializingPaused: initializingPaused,
downloadSpeedBytesPerSecond: downloadSpeedBytesPerSecond,
uploadSpeedBytesPerSecond: uploadSpeedBytesPerSecond,
error: error,
Expand Down
Loading
Loading