mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-08-03 17:20:16 +08:00
refa: download video (#1737)
* opt: save pb danmaku * refa: download video * opt: replaceAll * fix: wait delete * opt: remove completer * fix: index.json * tweaks Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> --------- Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
committed by
GitHub
parent
37b1228552
commit
407b31c5c1
@@ -119,66 +119,69 @@ class BiliDownloadEntryInfo {
|
|||||||
required DownloadService downloadService,
|
required DownloadService downloadService,
|
||||||
required bool isPage,
|
required bool isPage,
|
||||||
}) {
|
}) {
|
||||||
return Obx(() {
|
return RepaintBoundary(
|
||||||
final curDownload = downloadService.curDownload.value;
|
// TODO: refresh `downloadedBytes` only cause unnecessary repaint
|
||||||
final isCurr =
|
child: Obx(() {
|
||||||
curDownload != null &&
|
final curDownload = downloadService.curDownload.value;
|
||||||
(isPage ? curDownload.pageId == pageId : curDownload.cid == cid);
|
final isCurr =
|
||||||
late final status = curDownload?.status;
|
curDownload != null &&
|
||||||
late final isDownloading = status == DownloadStatus.downloading;
|
(isPage ? curDownload.pageId == pageId : curDownload.cid == cid);
|
||||||
late final color = isCurr && status != DownloadStatus.pause
|
late final status = curDownload?.status;
|
||||||
? theme.colorScheme.primary
|
late final isDownloading = status == DownloadStatus.downloading;
|
||||||
: theme.colorScheme.outline;
|
late final color = isCurr && status != DownloadStatus.pause
|
||||||
return Column(
|
? theme.colorScheme.primary
|
||||||
spacing: 6,
|
: theme.colorScheme.outline;
|
||||||
mainAxisSize: MainAxisSize.min,
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
spacing: 6,
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
children: [
|
||||||
children: [
|
Row(
|
||||||
Text(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
isCurr ? status!.message : '暂停中',
|
children: [
|
||||||
style: TextStyle(
|
Text(
|
||||||
fontSize: 12,
|
isCurr ? status!.message : '暂停中',
|
||||||
height: 1,
|
style: TextStyle(
|
||||||
color: color,
|
fontSize: 12,
|
||||||
|
height: 1,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
Text(
|
isCurr
|
||||||
isCurr
|
? isDownloading || status == DownloadStatus.pause
|
||||||
? isDownloading || status == DownloadStatus.pause
|
? ' ${CacheManager.formatSize(curDownload.downloadedBytes)}/${CacheManager.formatSize(curDownload.totalBytes)}'
|
||||||
? ' ${CacheManager.formatSize(curDownload.downloadedBytes)}/${CacheManager.formatSize(curDownload.totalBytes)}'
|
: ''
|
||||||
: ''
|
: totalBytes == 0
|
||||||
: totalBytes == 0
|
? ''
|
||||||
? ''
|
: ' ${CacheManager.formatSize(downloadedBytes)}/${CacheManager.formatSize(totalBytes)}',
|
||||||
: ' ${CacheManager.formatSize(downloadedBytes)}/${CacheManager.formatSize(totalBytes)}',
|
style: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 12,
|
||||||
fontSize: 12,
|
height: 1,
|
||||||
height: 1,
|
color: color,
|
||||||
color: color,
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
LinearProgressIndicator(
|
||||||
LinearProgressIndicator(
|
// ignore: deprecated_member_use
|
||||||
// ignore: deprecated_member_use
|
year2023: true,
|
||||||
year2023: true,
|
minHeight: 2.5,
|
||||||
minHeight: 2.5,
|
borderRadius: StyleString.mdRadius,
|
||||||
borderRadius: StyleString.mdRadius,
|
color: color,
|
||||||
color: color,
|
backgroundColor: theme.highlightColor,
|
||||||
backgroundColor: theme.highlightColor,
|
value: isCurr
|
||||||
value: isCurr
|
? curDownload.totalBytes == 0
|
||||||
? curDownload.totalBytes == 0
|
? 0
|
||||||
? 0
|
: curDownload.downloadedBytes / curDownload.totalBytes
|
||||||
: curDownload.downloadedBytes / curDownload.totalBytes
|
: totalBytes == 0
|
||||||
: totalBytes == 0
|
? 0
|
||||||
? 0
|
: downloadedBytes / totalBytes,
|
||||||
: downloadedBytes / totalBytes,
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
}),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
BiliDownloadEntryInfo({
|
BiliDownloadEntryInfo({
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io' show File;
|
import 'dart:io' show File;
|
||||||
|
|
||||||
import 'package:PiliPlus/grpc/bilibili/community/service/dm/v1.pb.dart';
|
import 'package:PiliPlus/grpc/bilibili/community/service/dm/v1.pb.dart';
|
||||||
import 'package:PiliPlus/grpc/dm.dart';
|
import 'package:PiliPlus/grpc/dm.dart';
|
||||||
import 'package:PiliPlus/plugin/pl_player/controller.dart';
|
import 'package:PiliPlus/plugin/pl_player/controller.dart';
|
||||||
import 'package:PiliPlus/utils/accounts.dart';
|
import 'package:PiliPlus/utils/accounts.dart';
|
||||||
import 'package:fixnum/fixnum.dart' show Int64;
|
import 'package:PiliPlus/utils/path_utils.dart';
|
||||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:xml/xml.dart';
|
|
||||||
|
|
||||||
class PlDanmakuController {
|
class PlDanmakuController {
|
||||||
PlDanmakuController(
|
PlDanmakuController(
|
||||||
@@ -36,7 +34,7 @@ class PlDanmakuController {
|
|||||||
requestedSeg.clear();
|
requestedSeg.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
int calcSegment(int progress) {
|
static int calcSegment(int progress) {
|
||||||
return progress ~/ segmentLength;
|
return progress ~/ segmentLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +100,7 @@ class PlDanmakuController {
|
|||||||
|
|
||||||
List<DanmakuElem>? getCurrentDanmaku(int progress) {
|
List<DanmakuElem>? getCurrentDanmaku(int progress) {
|
||||||
if (isFileSource) {
|
if (isFileSource) {
|
||||||
initXmlDmIfNeeded();
|
initFileDmIfNeeded();
|
||||||
} else {
|
} else {
|
||||||
final int segmentIndex = calcSegment(progress);
|
final int segmentIndex = calcSegment(progress);
|
||||||
if (!requestedSeg.contains(segmentIndex)) {
|
if (!requestedSeg.contains(segmentIndex)) {
|
||||||
@@ -115,68 +113,24 @@ class PlDanmakuController {
|
|||||||
|
|
||||||
bool closed = false;
|
bool closed = false;
|
||||||
|
|
||||||
late bool _xmlDmLoaded = false;
|
bool _fileDmLoaded = false;
|
||||||
|
|
||||||
void initXmlDmIfNeeded() {
|
void initFileDmIfNeeded() {
|
||||||
if (_xmlDmLoaded) return;
|
if (_fileDmLoaded) return;
|
||||||
_xmlDmLoaded = true;
|
_fileDmLoaded = true;
|
||||||
_initXmlDm();
|
_initFileDm();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initXmlDm() async {
|
Future<void> _initFileDm() async {
|
||||||
try {
|
try {
|
||||||
final file = File(path.join(plPlayerController.dirPath!, 'danmaku.xml'));
|
final file = File(
|
||||||
final stream = file.openRead().transform(utf8.decoder);
|
path.join(plPlayerController.dirPath!, PathUtils.danmakuName),
|
||||||
final buffer = StringBuffer();
|
);
|
||||||
await for (final chunk in stream) {
|
if (!file.existsSync()) return;
|
||||||
if (closed) {
|
final bytes = await file.readAsBytes();
|
||||||
return;
|
if (bytes.isEmpty) return;
|
||||||
}
|
final elem = DmSegMobileReply.fromBuffer(bytes).elems;
|
||||||
buffer.write(chunk);
|
handleDanmaku(elem);
|
||||||
}
|
|
||||||
if (closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final xmlString = buffer.toString();
|
|
||||||
final document = XmlDocument.parse(xmlString);
|
|
||||||
final danmakus = document.findAllElements('d').toList();
|
|
||||||
final elems = <DanmakuElem>[];
|
|
||||||
for (final dm in danmakus) {
|
|
||||||
if (closed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final pAttr = dm.getAttribute('p');
|
|
||||||
if (pAttr != null) {
|
|
||||||
final parts = pAttr.split(',');
|
|
||||||
final progress = double.parse(parts[0]); // sec
|
|
||||||
final mode = int.parse(parts[1]);
|
|
||||||
final fontsize = int.parse(parts[2]);
|
|
||||||
final color = int.parse(parts[3]);
|
|
||||||
// final ctime = int.parse(parts[4]);
|
|
||||||
// final pool = int.parse(parts[5]);
|
|
||||||
final midHash = parts[6];
|
|
||||||
final id = int.parse(parts[7]);
|
|
||||||
final weight = int.parse(parts[8]);
|
|
||||||
final content = dm.innerText;
|
|
||||||
elems.add(
|
|
||||||
DanmakuElem(
|
|
||||||
progress: (progress * 1000).toInt(),
|
|
||||||
mode: mode,
|
|
||||||
fontsize: fontsize,
|
|
||||||
color: color,
|
|
||||||
midHash: midHash,
|
|
||||||
id: Int64(id),
|
|
||||||
weight: weight,
|
|
||||||
content: content,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
if (kDebugMode) rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handleDanmaku(elems);
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (kDebugMode) rethrow;
|
if (kDebugMode) rethrow;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ class _PlDanmakuState extends State<PlDanmaku> {
|
|||||||
);
|
);
|
||||||
if (playerController.enableShowDanmaku.value) {
|
if (playerController.enableShowDanmaku.value) {
|
||||||
if (widget.isFileSource) {
|
if (widget.isFileSource) {
|
||||||
_plDanmakuController.initXmlDmIfNeeded();
|
_plDanmakuController.initFileDmIfNeeded();
|
||||||
} else {
|
} else {
|
||||||
_plDanmakuController.queryDanmaku(
|
_plDanmakuController.queryDanmaku(
|
||||||
_plDanmakuController.calcSegment(
|
PlDanmakuController.calcSegment(
|
||||||
playerController.position.value.inMilliseconds,
|
playerController.position.value.inMilliseconds,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:get/get.dart';
|
|||||||
|
|
||||||
class DownloadPageController extends GetxController {
|
class DownloadPageController extends GetxController {
|
||||||
final _downloadService = Get.find<DownloadService>();
|
final _downloadService = Get.find<DownloadService>();
|
||||||
late final StreamSubscription _sub;
|
|
||||||
final pages = RxList<DownloadPageInfo>();
|
final pages = RxList<DownloadPageInfo>();
|
||||||
final flag = RxInt(0);
|
final flag = RxInt(0);
|
||||||
|
|
||||||
@@ -14,14 +13,12 @@ class DownloadPageController extends GetxController {
|
|||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
_loadList();
|
_loadList();
|
||||||
_sub = _downloadService.downloaFlag.listen((_) {
|
_downloadService.flagNotifier.add(_loadList);
|
||||||
_loadList();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
_sub.cancel();
|
_downloadService.flagNotifier.remove(_loadList);
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:PiliPlus/common/constants.dart';
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||||
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
|
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
|
||||||
@@ -10,12 +12,15 @@ import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart';
|
|||||||
import 'package:PiliPlus/services/download/download_service.dart';
|
import 'package:PiliPlus/services/download/download_service.dart';
|
||||||
import 'package:PiliPlus/utils/cache_manager.dart';
|
import 'package:PiliPlus/utils/cache_manager.dart';
|
||||||
import 'package:PiliPlus/utils/duration_utils.dart';
|
import 'package:PiliPlus/utils/duration_utils.dart';
|
||||||
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
import 'package:PiliPlus/utils/page_utils.dart';
|
import 'package:PiliPlus/utils/page_utils.dart';
|
||||||
|
import 'package:PiliPlus/utils/path_utils.dart';
|
||||||
import 'package:PiliPlus/utils/storage.dart';
|
import 'package:PiliPlus/utils/storage.dart';
|
||||||
import 'package:PiliPlus/utils/utils.dart';
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
class DetailItem extends StatelessWidget {
|
class DetailItem extends StatelessWidget {
|
||||||
const DetailItem({
|
const DetailItem({
|
||||||
@@ -141,11 +146,35 @@ class DetailItem extends StatelessWidget {
|
|||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: StyleString.aspectRatio,
|
aspectRatio: StyleString.aspectRatio,
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) => NetworkImgLayer(
|
builder: (context, constraints) {
|
||||||
src: entry.cover,
|
final cover = File(
|
||||||
width: constraints.maxWidth,
|
path.join(entry.entryDirPath, PathUtils.coverName),
|
||||||
height: constraints.maxHeight,
|
);
|
||||||
),
|
return cover.existsSync()
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: StyleString.mdRadius,
|
||||||
|
child: Image.file(
|
||||||
|
cover,
|
||||||
|
width: constraints.maxWidth,
|
||||||
|
height: constraints.maxHeight,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
cacheHeight: constraints.maxWidth.cacheSize(
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
colorBlendMode: NetworkImgLayer.reduce
|
||||||
|
? BlendMode.modulate
|
||||||
|
: null,
|
||||||
|
color: NetworkImgLayer.reduce
|
||||||
|
? NetworkImgLayer.reduceLuxColor
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: NetworkImgLayer(
|
||||||
|
src: entry.cover,
|
||||||
|
width: constraints.maxWidth,
|
||||||
|
height: constraints.maxHeight,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (entry.videoQuality case final videoQuality?)
|
if (entry.videoQuality case final videoQuality?)
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ List<SettingsModel> get extraSettings => [
|
|||||||
if (downloadPath == defPath) return;
|
if (downloadPath == defPath) return;
|
||||||
downloadPath = defPath;
|
downloadPath = defPath;
|
||||||
setState();
|
setState();
|
||||||
Get.find<DownloadService>().readDownloadList();
|
Get.find<DownloadService>().initDownloadList();
|
||||||
GStorage.setting.delete(SettingBoxKey.downloadPath);
|
GStorage.setting.delete(SettingBoxKey.downloadPath);
|
||||||
},
|
},
|
||||||
dense: true,
|
dense: true,
|
||||||
@@ -102,7 +102,7 @@ List<SettingsModel> get extraSettings => [
|
|||||||
if (path == null || path == downloadPath) return;
|
if (path == null || path == downloadPath) return;
|
||||||
downloadPath = path;
|
downloadPath = path;
|
||||||
setState();
|
setState();
|
||||||
Get.find<DownloadService>().readDownloadList();
|
Get.find<DownloadService>().initDownloadList();
|
||||||
GStorage.setting.put(SettingBoxKey.downloadPath, path);
|
GStorage.setting.put(SettingBoxKey.downloadPath, path);
|
||||||
},
|
},
|
||||||
dense: true,
|
dense: true,
|
||||||
|
|||||||
@@ -130,9 +130,10 @@ class _DownloadPanelState extends State<DownloadPanel> {
|
|||||||
style: const TextStyle(height: 1),
|
style: const TextStyle(height: 1),
|
||||||
strutStyle: const StrutStyle(height: 1, leading: 0),
|
strutStyle: const StrutStyle(height: 1, leading: 0),
|
||||||
),
|
),
|
||||||
const Icon(
|
Icon(
|
||||||
size: 18,
|
size: 18,
|
||||||
Icons.keyboard_arrow_down,
|
Icons.keyboard_arrow_down,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:PiliPlus/common/constants.dart';
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/widgets/badge.dart';
|
import 'package:PiliPlus/common/widgets/badge.dart';
|
||||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||||
@@ -6,8 +8,11 @@ import 'package:PiliPlus/models/common/video/video_quality.dart';
|
|||||||
import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart';
|
import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart';
|
||||||
import 'package:PiliPlus/pages/video/introduction/local/controller.dart';
|
import 'package:PiliPlus/pages/video/introduction/local/controller.dart';
|
||||||
import 'package:PiliPlus/utils/duration_utils.dart';
|
import 'package:PiliPlus/utils/duration_utils.dart';
|
||||||
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
|
import 'package:PiliPlus/utils/path_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
class LocalIntroPanel extends StatefulWidget {
|
class LocalIntroPanel extends StatefulWidget {
|
||||||
const LocalIntroPanel({super.key, required this.heroTag});
|
const LocalIntroPanel({super.key, required this.heroTag});
|
||||||
@@ -49,6 +54,7 @@ class _LocalIntroPanelState extends State<LocalIntroPanel>
|
|||||||
BiliDownloadEntryInfo entry,
|
BiliDownloadEntryInfo entry,
|
||||||
) {
|
) {
|
||||||
final outline = theme.colorScheme.outline;
|
final outline = theme.colorScheme.outline;
|
||||||
|
final cover = File(path.join(entry.entryDirPath, PathUtils.coverName));
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@@ -73,11 +79,28 @@ class _LocalIntroPanelState extends State<LocalIntroPanel>
|
|||||||
Stack(
|
Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
NetworkImgLayer(
|
cover.existsSync()
|
||||||
src: entry.cover,
|
? ClipRRect(
|
||||||
width: 140.8,
|
borderRadius: StyleString.mdRadius,
|
||||||
height: 88,
|
child: Image.file(
|
||||||
),
|
cover,
|
||||||
|
width: 140.8,
|
||||||
|
height: 88,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
cacheHeight: 140.8.cacheSize(context),
|
||||||
|
colorBlendMode: NetworkImgLayer.reduce
|
||||||
|
? BlendMode.modulate
|
||||||
|
: null,
|
||||||
|
color: NetworkImgLayer.reduce
|
||||||
|
? NetworkImgLayer.reduceLuxColor
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: NetworkImgLayer(
|
||||||
|
src: entry.cover,
|
||||||
|
width: 140.8,
|
||||||
|
height: 88,
|
||||||
|
),
|
||||||
PBadge(
|
PBadge(
|
||||||
text: DurationUtils.formatDuration(
|
text: DurationUtils.formatDuration(
|
||||||
entry.totalTimeMilli ~/ 1000,
|
entry.totalTimeMilli ~/ 1000,
|
||||||
|
|||||||
@@ -1308,7 +1308,7 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
|||||||
),
|
),
|
||||||
if (!videoDetailController.isFileSource)
|
if (!videoDetailController.isFileSource)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
onTap: () => videoDetailController.onDownload(context),
|
onTap: () => videoDetailController.onDownload(this.context),
|
||||||
child: const Text('缓存视频'),
|
child: const Text('缓存视频'),
|
||||||
),
|
),
|
||||||
if (videoDetailController.cover.value.isNotEmpty)
|
if (videoDetailController.cover.value.isNotEmpty)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'dart:async' show Completer, StreamSubscription;
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:PiliPlus/http/init.dart';
|
import 'package:PiliPlus/http/init.dart';
|
||||||
@@ -9,186 +9,97 @@ import 'package:dio/dio.dart';
|
|||||||
class DownloadManager {
|
class DownloadManager {
|
||||||
final String url;
|
final String url;
|
||||||
final String path;
|
final String path;
|
||||||
final Function({required int progress, required int total}) onTaskRunning;
|
final void Function(int, int)? onReceiveProgress;
|
||||||
final Function() onTaskComplete;
|
final void Function([Object? error]) onDone;
|
||||||
final Function({
|
|
||||||
required int progress,
|
|
||||||
required int total,
|
|
||||||
required Object error,
|
|
||||||
})
|
|
||||||
onTaskError;
|
|
||||||
|
|
||||||
bool _closed = false;
|
DownloadStatus _status = DownloadStatus.downloading;
|
||||||
DownloadStatus _status = DownloadStatus.wait;
|
|
||||||
DownloadStatus get status => _status;
|
DownloadStatus get status => _status;
|
||||||
CancelToken? _cancelToken;
|
final _cancelToken = CancelToken();
|
||||||
Completer? _completer;
|
late Future<void> task;
|
||||||
|
|
||||||
DownloadManager({
|
DownloadManager({
|
||||||
required this.url,
|
required this.url,
|
||||||
required this.path,
|
required this.path,
|
||||||
required this.onTaskRunning,
|
required this.onReceiveProgress,
|
||||||
required this.onTaskComplete,
|
required this.onDone,
|
||||||
required this.onTaskError,
|
}) {
|
||||||
});
|
task = _start();
|
||||||
|
|
||||||
void _complete() {
|
|
||||||
if (_completer?.isCompleted == false) {
|
|
||||||
_completer?.complete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> start() async {
|
Future<void> _start() async {
|
||||||
_completer = Completer();
|
int received;
|
||||||
_cancelToken = CancelToken();
|
|
||||||
_status = DownloadStatus.downloading;
|
|
||||||
|
|
||||||
final file = File(path);
|
final file = File(path);
|
||||||
// If the file already exists, the method fails.
|
if (file.existsSync()) {
|
||||||
if (!file.existsSync()) {
|
received = await file.length();
|
||||||
|
} else {
|
||||||
file.createSync(recursive: true);
|
file.createSync(recursive: true);
|
||||||
|
received = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int downloadedSize = await file.length();
|
final sink = file.openWrite(
|
||||||
|
mode: received == 0 ? FileMode.writeOnly : FileMode.writeOnlyAppend,
|
||||||
// Shouldn't call file.writeAsBytesSync(list, flush: flush),
|
|
||||||
// because it can write all bytes by once. Consider that the file is
|
|
||||||
// a very big size (up to 1 Gigabytes), it will be expensive in memory.
|
|
||||||
RandomAccessFile raf = file.openSync(
|
|
||||||
mode: downloadedSize == 0 ? FileMode.write : FileMode.append,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void>? asyncWrite;
|
Future<void> onError(Object e, {bool delete = false}) async {
|
||||||
Future<void> closeAndDelete({bool delete = false}) async {
|
try {
|
||||||
if (!_closed) {
|
await sink.close();
|
||||||
_closed = true;
|
} catch (_) {}
|
||||||
await asyncWrite;
|
if (_status == DownloadStatus.downloading) {
|
||||||
await raf.close().catchError((_) => raf);
|
_status = DownloadStatus.failDownload;
|
||||||
if (delete && file.existsSync()) {
|
if (delete && file.existsSync()) {
|
||||||
await file.delete().catchError((_) => file);
|
await file.tryDel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onDone(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Response<ResponseBody> response;
|
Response<ResponseBody> response;
|
||||||
try {
|
try {
|
||||||
response = await Request.dio.get<ResponseBody>(
|
response = await Request.dio.get<ResponseBody>(
|
||||||
url.http2https,
|
url.http2https,
|
||||||
options: Options(
|
options: Options(
|
||||||
headers: {'range': 'bytes=$downloadedSize-'},
|
headers: {'range': 'bytes=$received-'},
|
||||||
responseType: ResponseType.stream,
|
responseType: ResponseType.stream,
|
||||||
validateStatus: (status) {
|
validateStatus: (status) =>
|
||||||
return status == 416 ||
|
status != null &&
|
||||||
(status != null && status >= 200 && status < 300);
|
(status == 416 || (status >= 200 && status < 300)),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
cancelToken: _cancelToken,
|
cancelToken: _cancelToken,
|
||||||
);
|
);
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
final isFailed = e.response?.statusCode != 416;
|
await onError(e, delete: true);
|
||||||
if (isFailed) {
|
|
||||||
_status = DownloadStatus.failDownload;
|
|
||||||
onTaskError(progress: 0, total: 0, error: e);
|
|
||||||
} else {
|
|
||||||
_status = DownloadStatus.completed;
|
|
||||||
onTaskComplete();
|
|
||||||
}
|
|
||||||
closeAndDelete(delete: isFailed).whenComplete(_complete);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final data = response.data!;
|
||||||
|
final contentLength = data.contentLength + received;
|
||||||
|
|
||||||
int received = downloadedSize;
|
if (received == 0) {
|
||||||
|
onReceiveProgress?.call(0, contentLength);
|
||||||
// Stream<Uint8List>
|
|
||||||
final stream = response.data!.stream;
|
|
||||||
|
|
||||||
final total =
|
|
||||||
int.parse(response.headers.value(Headers.contentLengthHeader) ?? '0') +
|
|
||||||
downloadedSize;
|
|
||||||
|
|
||||||
if (downloadedSize == 0) {
|
|
||||||
onTaskRunning(progress: 0, total: total);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
late StreamSubscription subscription;
|
try {
|
||||||
subscription = stream.listen(
|
await for (final chunk in data.stream) {
|
||||||
(data) {
|
sink.add(chunk);
|
||||||
subscription.pause();
|
received += chunk.length;
|
||||||
// Write file asynchronously
|
onReceiveProgress?.call(received, contentLength);
|
||||||
asyncWrite = raf
|
}
|
||||||
.writeFrom(data)
|
await sink.close();
|
||||||
.then((result) async {
|
_status = DownloadStatus.completed;
|
||||||
// Notify progress
|
onDone();
|
||||||
received += data.length;
|
} catch (e) {
|
||||||
onTaskRunning(progress: received, total: total);
|
await onError(e);
|
||||||
|
return;
|
||||||
raf = result;
|
|
||||||
if (_cancelToken != null && !_cancelToken!.isCancelled) {
|
|
||||||
subscription.resume();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catchError((Object e) async {
|
|
||||||
try {
|
|
||||||
await subscription.cancel().catchError((_) {});
|
|
||||||
_closed = true;
|
|
||||||
await raf.close().catchError((_) => raf);
|
|
||||||
if (file.existsSync()) {
|
|
||||||
await file.delete().catchError((_) => file);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_status = DownloadStatus.failDownload;
|
|
||||||
onTaskError(progress: received, total: total, error: e);
|
|
||||||
} finally {
|
|
||||||
_complete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onDone: () async {
|
|
||||||
try {
|
|
||||||
await asyncWrite;
|
|
||||||
_closed = true;
|
|
||||||
await raf.close().catchError((_) => raf);
|
|
||||||
_status = DownloadStatus.completed;
|
|
||||||
onTaskComplete();
|
|
||||||
} catch (e) {
|
|
||||||
_status = DownloadStatus.failDownload;
|
|
||||||
onTaskError(progress: received, total: total, error: e);
|
|
||||||
} finally {
|
|
||||||
_complete();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (e) async {
|
|
||||||
try {
|
|
||||||
await closeAndDelete(delete: true);
|
|
||||||
} catch (e) {
|
|
||||||
_cancel();
|
|
||||||
_status = DownloadStatus.failDownload;
|
|
||||||
onTaskError(progress: received, total: total, error: e);
|
|
||||||
} finally {
|
|
||||||
_complete();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cancelOnError: true,
|
|
||||||
);
|
|
||||||
_cancelToken?.whenCancel.then((_) async {
|
|
||||||
await subscription.cancel();
|
|
||||||
await closeAndDelete();
|
|
||||||
_complete();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void>? _cancel() {
|
|
||||||
if (_cancelToken != null) {
|
|
||||||
_cancelToken?.cancel();
|
|
||||||
_cancelToken = null;
|
|
||||||
}
|
}
|
||||||
return _completer?.future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void>? cancel({required bool isDelete}) {
|
Future<void> cancel({required bool isDelete}) {
|
||||||
if (!isDelete && _status == DownloadStatus.downloading) {
|
if (!isDelete && _status == DownloadStatus.downloading) {
|
||||||
_status = DownloadStatus.pause;
|
_status = DownloadStatus.pause;
|
||||||
}
|
}
|
||||||
return _cancel();
|
if (!_cancelToken.isCancelled) {
|
||||||
|
_cancelToken.cancel();
|
||||||
|
}
|
||||||
|
return task;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert' show jsonDecode, utf8;
|
import 'dart:convert' show jsonDecode, jsonEncode;
|
||||||
import 'dart:io' show Directory, File, FileSystemEntity;
|
import 'dart:io' show Directory, File;
|
||||||
|
|
||||||
|
import 'package:PiliPlus/grpc/dm.dart';
|
||||||
import 'package:PiliPlus/http/download.dart';
|
import 'package:PiliPlus/http/download.dart';
|
||||||
import 'package:PiliPlus/http/init.dart';
|
import 'package:PiliPlus/http/init.dart';
|
||||||
import 'package:PiliPlus/models/common/video/video_quality.dart';
|
import 'package:PiliPlus/models/common/video/video_quality.dart';
|
||||||
@@ -12,14 +13,13 @@ import 'package:PiliPlus/models_new/pgc/pgc_info_model/result.dart';
|
|||||||
import 'package:PiliPlus/models_new/video/video_detail/data.dart';
|
import 'package:PiliPlus/models_new/video/video_detail/data.dart';
|
||||||
import 'package:PiliPlus/models_new/video/video_detail/episode.dart' as ugc;
|
import 'package:PiliPlus/models_new/video/video_detail/episode.dart' as ugc;
|
||||||
import 'package:PiliPlus/models_new/video/video_detail/page.dart';
|
import 'package:PiliPlus/models_new/video/video_detail/page.dart';
|
||||||
|
import 'package:PiliPlus/pages/danmaku/controller.dart';
|
||||||
import 'package:PiliPlus/services/download/download_manager.dart';
|
import 'package:PiliPlus/services/download/download_manager.dart';
|
||||||
import 'package:PiliPlus/utils/extension.dart';
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
import 'package:PiliPlus/utils/id_utils.dart';
|
import 'package:PiliPlus/utils/id_utils.dart';
|
||||||
import 'package:PiliPlus/utils/path_utils.dart';
|
import 'package:PiliPlus/utils/path_utils.dart';
|
||||||
import 'package:PiliPlus/utils/utils.dart';
|
|
||||||
import 'package:archive/archive.dart' show Inflate;
|
|
||||||
import 'package:dio/dio.dart' show Options, ResponseType;
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
@@ -30,12 +30,10 @@ import 'package:synchronized/synchronized.dart';
|
|||||||
class DownloadService extends GetxService {
|
class DownloadService extends GetxService {
|
||||||
static const _entryFile = 'entry.json';
|
static const _entryFile = 'entry.json';
|
||||||
static const _indexFile = 'index.json';
|
static const _indexFile = 'index.json';
|
||||||
static const _danmakuFile = 'danmaku.xml';
|
|
||||||
static const _coverFile = 'cover.jpg';
|
|
||||||
|
|
||||||
final _lock = Lock();
|
final _lock = Lock();
|
||||||
|
|
||||||
final downloaFlag = RxnInt();
|
final flagNotifier = <void Function()>{};
|
||||||
final waitDownloadQueue = <BiliDownloadEntryInfo>[];
|
final waitDownloadQueue = <BiliDownloadEntryInfo>[];
|
||||||
final downloadList = RxList<BiliDownloadEntryInfo>();
|
final downloadList = RxList<BiliDownloadEntryInfo>();
|
||||||
|
|
||||||
@@ -51,41 +49,40 @@ class DownloadService extends GetxService {
|
|||||||
DownloadManager? _downloadManager;
|
DownloadManager? _downloadManager;
|
||||||
DownloadManager? _audioDownloadManager;
|
DownloadManager? _audioDownloadManager;
|
||||||
|
|
||||||
Completer? _completer;
|
late Future<void> waitForInitialization;
|
||||||
Future<void>? get waitForInitialization => _completer?.future;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
readDownloadList();
|
initDownloadList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> readDownloadList() async {
|
void initDownloadList() {
|
||||||
_completer = Completer();
|
waitForInitialization = _readDownloadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _readDownloadList() async {
|
||||||
final downloadDir = Directory(await _getDownloadPath());
|
final downloadDir = Directory(await _getDownloadPath());
|
||||||
final list = <BiliDownloadEntryInfo>[];
|
final list = <BiliDownloadEntryInfo>[];
|
||||||
for (final dir in downloadDir.listSync()) {
|
await for (final dir in downloadDir.list()) {
|
||||||
if (dir is Directory) {
|
if (dir is Directory) {
|
||||||
list.addAll(await _readDownloadDirectory(dir));
|
list.addAll(await _readDownloadDirectory(dir));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
downloadList.value = list
|
downloadList.value = list
|
||||||
..sort((a, b) => b.timeUpdateStamp.compareTo(a.timeUpdateStamp));
|
..sort((a, b) => b.timeUpdateStamp.compareTo(a.timeUpdateStamp));
|
||||||
if (!_completer!.isCompleted) {
|
|
||||||
_completer!.complete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<BiliDownloadEntryInfo>> _readDownloadDirectory(
|
Future<List<BiliDownloadEntryInfo>> _readDownloadDirectory(
|
||||||
FileSystemEntity pageDir,
|
Directory pageDir,
|
||||||
) async {
|
) async {
|
||||||
final result = <BiliDownloadEntryInfo>[];
|
final result = <BiliDownloadEntryInfo>[];
|
||||||
|
|
||||||
if (!pageDir.existsSync() || pageDir is! Directory) {
|
if (!pageDir.existsSync()) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final entryDir in pageDir.listSync()) {
|
await for (final entryDir in pageDir.list()) {
|
||||||
if (entryDir is Directory) {
|
if (entryDir is Directory) {
|
||||||
final entryFile = File(path.join(entryDir.path, _entryFile));
|
final entryFile = File(path.join(entryDir.path, _entryFile));
|
||||||
if (entryFile.existsSync()) {
|
if (entryFile.existsSync()) {
|
||||||
@@ -96,7 +93,7 @@ class DownloadService extends GetxService {
|
|||||||
..entryDirPath = entryDir.path;
|
..entryDirPath = entryDir.path;
|
||||||
result.add(entry);
|
result.add(entry);
|
||||||
if (!entry.isCompleted) {
|
if (!entry.isCompleted) {
|
||||||
waitDownloadQueue.add(entry);
|
waitDownloadQueue.add(entry..status = DownloadStatus.wait);
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (kDebugMode) rethrow;
|
if (kDebugMode) rethrow;
|
||||||
@@ -141,7 +138,7 @@ class DownloadService extends GetxService {
|
|||||||
downloadedBytes: 0,
|
downloadedBytes: 0,
|
||||||
title: videoDetail?.title ?? videoArc!.title!,
|
title: videoDetail?.title ?? videoArc!.title!,
|
||||||
typeTag: videoQuality.code.toString(),
|
typeTag: videoQuality.code.toString(),
|
||||||
cover: videoDetail?.pic ?? videoArc!.cover!,
|
cover: (videoDetail?.pic ?? videoArc!.cover!).http2https,
|
||||||
preferedVideoQuality: videoQuality.code,
|
preferedVideoQuality: videoQuality.code,
|
||||||
qualityPithyDescription: videoQuality.desc,
|
qualityPithyDescription: videoQuality.desc,
|
||||||
guessedTotalBytes: 0,
|
guessedTotalBytes: 0,
|
||||||
@@ -234,14 +231,13 @@ class DownloadService extends GetxService {
|
|||||||
Future<void> _createDownload(BiliDownloadEntryInfo entry) async {
|
Future<void> _createDownload(BiliDownloadEntryInfo entry) async {
|
||||||
final entryDir = await _getDownloadEntryDir(entry);
|
final entryDir = await _getDownloadEntryDir(entry);
|
||||||
final entryJsonFile = File(path.join(entryDir.path, _entryFile));
|
final entryJsonFile = File(path.join(entryDir.path, _entryFile));
|
||||||
final entryJsonStr = Utils.jsonEncoder.convert(entry.toJson());
|
await entryJsonFile.writeAsString(jsonEncode(entry.toJson()));
|
||||||
await entryJsonFile.writeAsBytes(utf8.encode(entryJsonStr));
|
|
||||||
entry
|
entry
|
||||||
..pageDirPath = entryDir.parent.path
|
..pageDirPath = entryDir.parent.path
|
||||||
..entryDirPath = entryDir.path
|
..entryDirPath = entryDir.path
|
||||||
..status = DownloadStatus.wait;
|
..status = DownloadStatus.wait;
|
||||||
downloadList.insert(0, entry);
|
downloadList.insert(0, entry);
|
||||||
downloaFlag.refresh();
|
flagNotifier.refresh();
|
||||||
final currStatus = curDownload.value?.status?.index;
|
final currStatus = curDownload.value?.status?.index;
|
||||||
if (currStatus == null || currStatus > 3) {
|
if (currStatus == null || currStatus > 3) {
|
||||||
startDownload(entry);
|
startDownload(entry);
|
||||||
@@ -269,7 +265,7 @@ class DownloadService extends GetxService {
|
|||||||
return pageDir;
|
return pageDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _getDownloadPath() async {
|
static Future<String> _getDownloadPath() async {
|
||||||
final dir = Directory(downloadPath);
|
final dir = Directory(downloadPath);
|
||||||
if (!dir.existsSync()) {
|
if (!dir.existsSync()) {
|
||||||
await dir.create(recursive: true);
|
await dir.create(recursive: true);
|
||||||
@@ -301,26 +297,38 @@ class DownloadService extends GetxService {
|
|||||||
if (cid == null) {
|
if (cid == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final danmakuXMLFile = File(path.join(entry.entryDirPath, _danmakuFile));
|
final danmakuFile = File(
|
||||||
if (isUpdate || !danmakuXMLFile.existsSync()) {
|
path.join(entry.entryDirPath, PathUtils.danmakuName),
|
||||||
|
);
|
||||||
|
if (isUpdate || !danmakuFile.existsSync()) {
|
||||||
try {
|
try {
|
||||||
if (!isUpdate) {
|
if (!isUpdate) {
|
||||||
_updateCurStatus(DownloadStatus.getDanmaku);
|
_updateCurStatus(DownloadStatus.getDanmaku);
|
||||||
}
|
}
|
||||||
final res = await Request().get(
|
final seg = (entry.totalTimeMilli / PlDanmakuController.segmentLength)
|
||||||
'https://comment.bilibili.com/$cid.xml',
|
.ceil();
|
||||||
options: Options(responseType: ResponseType.bytes),
|
|
||||||
);
|
final res = await Future.wait([
|
||||||
final xmlBytes = Inflate((res.data as Uint8List)).getBytes();
|
for (var i = 1; i <= seg; i++)
|
||||||
await danmakuXMLFile.writeAsBytes(xmlBytes);
|
DmGrpc.dmSegMobile(cid: cid, segmentIndex: i),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final danmaku = res.removeAt(0).data;
|
||||||
|
for (var i in res) {
|
||||||
|
if (!i.isSuccess) {
|
||||||
|
throw i.toString();
|
||||||
|
}
|
||||||
|
danmaku.elems.addAll(i.data.elems);
|
||||||
|
}
|
||||||
|
res.clear();
|
||||||
|
await danmakuFile.writeAsBytes(danmaku.writeToBuffer());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!isUpdate) {
|
if (!isUpdate) {
|
||||||
_updateCurStatus(DownloadStatus.failDanmaku);
|
_updateCurStatus(DownloadStatus.failDanmaku);
|
||||||
}
|
}
|
||||||
if (kDebugMode) {
|
if (kDebugMode) SmartDialog.showToast(e.toString());
|
||||||
SmartDialog.showToast(e.toString());
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,13 +339,22 @@ class DownloadService extends GetxService {
|
|||||||
required BiliDownloadEntryInfo entry,
|
required BiliDownloadEntryInfo entry,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
await Request.dio.download(
|
final filePath = path.join(entry.entryDirPath, PathUtils.coverName);
|
||||||
entry.cover.http2https,
|
if (File(filePath).existsSync()) {
|
||||||
path.join(entry.entryDirPath, _coverFile),
|
return true;
|
||||||
);
|
}
|
||||||
|
final file = (await DefaultCacheManager().getFileFromCache(
|
||||||
|
entry.cover,
|
||||||
|
))?.file;
|
||||||
|
if (file != null) {
|
||||||
|
await file.copy(filePath);
|
||||||
|
} else {
|
||||||
|
await Request.dio.download(entry.cover, filePath);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (_) {}
|
} catch (_) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startDownload(BiliDownloadEntryInfo entry) async {
|
Future<void> _startDownload(BiliDownloadEntryInfo entry) async {
|
||||||
@@ -357,18 +374,16 @@ class DownloadService extends GetxService {
|
|||||||
await videoDir.create(recursive: true);
|
await videoDir.create(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
final res = await Future.wait([
|
final coverTask = _downloadCover(entry: entry);
|
||||||
downloadDanmaku(entry: entry),
|
|
||||||
_downloadCover(entry: entry),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!res.first) {
|
if (!await downloadDanmaku(entry: entry)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final mediaJsonFile = File(path.join(videoDir.path, _indexFile));
|
final mediaJsonFile = File(path.join(videoDir.path, _indexFile));
|
||||||
final mediaJsonStr = Utils.jsonEncoder.convert(mediaFileInfo.toJson());
|
await Future.wait([
|
||||||
await mediaJsonFile.writeAsString(mediaJsonStr);
|
mediaJsonFile.writeAsString(jsonEncode(mediaFileInfo.toJson())),
|
||||||
|
coverTask,
|
||||||
|
]);
|
||||||
|
|
||||||
if (curDownload.value?.cid != entry.cid) {
|
if (curDownload.value?.cid != entry.cid) {
|
||||||
return;
|
return;
|
||||||
@@ -380,28 +395,25 @@ class DownloadService extends GetxService {
|
|||||||
_downloadManager = DownloadManager(
|
_downloadManager = DownloadManager(
|
||||||
url: first.url,
|
url: first.url,
|
||||||
path: path.join(videoDir.path, PathUtils.videoNameType1),
|
path: path.join(videoDir.path, PathUtils.videoNameType1),
|
||||||
onTaskRunning: _onTaskRunning,
|
onReceiveProgress: _onReceive,
|
||||||
onTaskComplete: _onTaskComplete,
|
onDone: _onDone,
|
||||||
onTaskError: _onTaskError,
|
);
|
||||||
)..start();
|
|
||||||
break;
|
break;
|
||||||
case Type2 mediaFileInfo:
|
case Type2 mediaFileInfo:
|
||||||
_downloadManager = DownloadManager(
|
_downloadManager = DownloadManager(
|
||||||
url: mediaFileInfo.video.first.baseUrl,
|
url: mediaFileInfo.video.first.baseUrl,
|
||||||
path: path.join(videoDir.path, PathUtils.videoNameType2),
|
path: path.join(videoDir.path, PathUtils.videoNameType2),
|
||||||
onTaskRunning: _onTaskRunning,
|
onReceiveProgress: _onReceive,
|
||||||
onTaskComplete: _onTaskComplete,
|
onDone: _onDone,
|
||||||
onTaskError: _onTaskError,
|
);
|
||||||
)..start();
|
|
||||||
final audio = mediaFileInfo.audio;
|
final audio = mediaFileInfo.audio;
|
||||||
if (audio != null && audio.isNotEmpty) {
|
if (audio != null && audio.isNotEmpty) {
|
||||||
_audioDownloadManager = DownloadManager(
|
_audioDownloadManager = DownloadManager(
|
||||||
url: audio.first.baseUrl,
|
url: audio.first.baseUrl,
|
||||||
path: path.join(videoDir.path, PathUtils.audioNameType2),
|
path: path.join(videoDir.path, PathUtils.audioNameType2),
|
||||||
onTaskRunning: _onAudioTaskRunning,
|
onReceiveProgress: null,
|
||||||
onTaskComplete: _onAudioTaskComplete,
|
onDone: _onAudioDone,
|
||||||
onTaskError: _onAudioTaskError,
|
);
|
||||||
)..start();
|
|
||||||
}
|
}
|
||||||
late final first = mediaFileInfo.video.first;
|
late final first = mediaFileInfo.video.first;
|
||||||
entry.pageData
|
entry.pageData
|
||||||
@@ -425,17 +437,14 @@ class DownloadService extends GetxService {
|
|||||||
|
|
||||||
Future<void> _updateBiliDownloadEntryJson(BiliDownloadEntryInfo entry) async {
|
Future<void> _updateBiliDownloadEntryJson(BiliDownloadEntryInfo entry) async {
|
||||||
final entryJsonFile = File(path.join(entry.entryDirPath, _entryFile));
|
final entryJsonFile = File(path.join(entry.entryDirPath, _entryFile));
|
||||||
final entryJsonStr = Utils.jsonEncoder.convert(entry.toJson());
|
await entryJsonFile.writeAsString(jsonEncode(entry.toJson()));
|
||||||
await entryJsonFile.writeAsString(entryJsonStr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onTaskRunning({required int progress, required int total}) {
|
void _onReceive(int progress, int total) {
|
||||||
if (progress == 0 && total != 0) {
|
|
||||||
if (curDownload.value case final curEntryInfo?) {
|
|
||||||
_updateBiliDownloadEntryJson(curEntryInfo..totalBytes = total);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (curDownload.value case final entry?) {
|
if (curDownload.value case final entry?) {
|
||||||
|
if (progress == 0 && total != 0) {
|
||||||
|
_updateBiliDownloadEntryJson(entry..totalBytes = total);
|
||||||
|
}
|
||||||
entry
|
entry
|
||||||
..downloadedBytes = progress
|
..downloadedBytes = progress
|
||||||
..status = DownloadStatus.downloading;
|
..status = DownloadStatus.downloading;
|
||||||
@@ -443,55 +452,38 @@ class DownloadService extends GetxService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onTaskComplete() {
|
void _onDone([Object? error]) {
|
||||||
final audioStatus = _audioDownloadManager?.status;
|
final status = switch (_audioDownloadManager?.status) {
|
||||||
final status = switch (audioStatus) {
|
|
||||||
DownloadStatus.downloading => DownloadStatus.audioDownloading,
|
DownloadStatus.downloading => DownloadStatus.audioDownloading,
|
||||||
DownloadStatus.failDownload => DownloadStatus.failDownloadAudio,
|
DownloadStatus.failDownload => DownloadStatus.failDownloadAudio,
|
||||||
null => DownloadStatus.completed,
|
_ => _downloadManager?.status ?? DownloadStatus.pause,
|
||||||
_ => audioStatus,
|
|
||||||
};
|
};
|
||||||
_updateCurStatus(status);
|
_updateCurStatus(status);
|
||||||
if (status == DownloadStatus.completed) {
|
|
||||||
_completeDownload();
|
if (curDownload.value case final curEntryInfo?) {
|
||||||
} else {
|
if (error == null) {
|
||||||
if (curDownload.value case final curEntryInfo?) {
|
curEntryInfo.downloadedBytes = curEntryInfo.totalBytes;
|
||||||
_updateBiliDownloadEntryJson(
|
}
|
||||||
curEntryInfo..downloadedBytes = curEntryInfo.totalBytes,
|
if (status == DownloadStatus.completed) {
|
||||||
);
|
_completeDownload();
|
||||||
|
} else {
|
||||||
|
_updateBiliDownloadEntryJson(curEntryInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onTaskError({
|
void _onAudioDone([Object? error]) {
|
||||||
required int progress,
|
|
||||||
required int total,
|
|
||||||
required Object error,
|
|
||||||
}) {
|
|
||||||
_updateCurStatus(DownloadStatus.failDownload);
|
|
||||||
if (curDownload.value case final curEntryInfo?) {
|
|
||||||
curEntryInfo
|
|
||||||
..totalBytes = total
|
|
||||||
..downloadedBytes = progress;
|
|
||||||
_updateBiliDownloadEntryJson(curEntryInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onAudioTaskRunning({required int progress, required int total}) {}
|
|
||||||
|
|
||||||
void _onAudioTaskComplete() {
|
|
||||||
if (_downloadManager?.status == DownloadStatus.completed) {
|
if (_downloadManager?.status == DownloadStatus.completed) {
|
||||||
_completeDownload();
|
if (error == null) {
|
||||||
}
|
_completeDownload();
|
||||||
}
|
} else {
|
||||||
|
final status = _audioDownloadManager?.status ?? DownloadStatus.pause;
|
||||||
void _onAudioTaskError({
|
_updateCurStatus(
|
||||||
required int progress,
|
status == DownloadStatus.failDownload
|
||||||
required int total,
|
? DownloadStatus.failDownloadAudio
|
||||||
required Object error,
|
: status,
|
||||||
}) {
|
);
|
||||||
if (_downloadManager?.status == DownloadStatus.completed) {
|
}
|
||||||
_updateCurStatus(DownloadStatus.failDownloadAudio);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,7 +496,7 @@ class DownloadService extends GetxService {
|
|||||||
..downloadedBytes = entry.totalBytes
|
..downloadedBytes = entry.totalBytes
|
||||||
..isCompleted = true;
|
..isCompleted = true;
|
||||||
await _updateBiliDownloadEntryJson(entry);
|
await _updateBiliDownloadEntryJson(entry);
|
||||||
downloaFlag.refresh();
|
flagNotifier.refresh();
|
||||||
curDownload.value = null;
|
curDownload.value = null;
|
||||||
_downloadManager = null;
|
_downloadManager = null;
|
||||||
_audioDownloadManager = null;
|
_audioDownloadManager = null;
|
||||||
@@ -514,7 +506,7 @@ class DownloadService extends GetxService {
|
|||||||
void _nextDownload() {
|
void _nextDownload() {
|
||||||
if (waitDownloadQueue.isNotEmpty) {
|
if (waitDownloadQueue.isNotEmpty) {
|
||||||
final next = waitDownloadQueue.removeAt(0);
|
final next = waitDownloadQueue.removeAt(0);
|
||||||
if (downloadList.contains(next)) {
|
if (!next.isCompleted && downloadList.contains(next)) {
|
||||||
startDownload(next);
|
startDownload(next);
|
||||||
} else {
|
} else {
|
||||||
_nextDownload();
|
_nextDownload();
|
||||||
@@ -530,7 +522,7 @@ class DownloadService extends GetxService {
|
|||||||
}
|
}
|
||||||
final downloadDir = Directory(entry.pageDirPath);
|
final downloadDir = Directory(entry.pageDirPath);
|
||||||
if (downloadDir.existsSync()) {
|
if (downloadDir.existsSync()) {
|
||||||
if (downloadDir.listSync().length <= 1) {
|
if (!await downloadDir.lengthGte(2)) {
|
||||||
await downloadDir.tryDel(recursive: true);
|
await downloadDir.tryDel(recursive: true);
|
||||||
} else {
|
} else {
|
||||||
final entryDir = Directory(entry.entryDirPath);
|
final entryDir = Directory(entry.entryDirPath);
|
||||||
@@ -541,13 +533,13 @@ class DownloadService extends GetxService {
|
|||||||
}
|
}
|
||||||
downloadList.remove(entry);
|
downloadList.remove(entry);
|
||||||
waitDownloadQueue.remove(entry);
|
waitDownloadQueue.remove(entry);
|
||||||
downloaFlag.refresh();
|
flagNotifier.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deletePage({required String pageDirPath}) async {
|
Future<void> deletePage({required String pageDirPath}) async {
|
||||||
await Directory(pageDirPath).tryDel(recursive: true);
|
await Directory(pageDirPath).tryDel(recursive: true);
|
||||||
downloadList.removeWhere((e) => e.pageDirPath == pageDirPath);
|
downloadList.removeWhere((e) => e.pageDirPath == pageDirPath);
|
||||||
downloaFlag.refresh();
|
flagNotifier.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> cancelDownload({
|
Future<void> cancelDownload({
|
||||||
@@ -574,3 +566,11 @@ class DownloadService extends GetxService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on Set<void Function()> {
|
||||||
|
void refresh() {
|
||||||
|
for (var i in this) {
|
||||||
|
i();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
abstract class Em {
|
abstract class Em {
|
||||||
static final _exp = RegExp('<[^>]*>([^<]*)</[^>]*>');
|
static final _exp = RegExp('<[^>]*>([^<]*)</[^>]*>');
|
||||||
|
static final _htmlRegExp = RegExp(r'&(lt|gt|quot|apos|nbsp|amp);');
|
||||||
|
|
||||||
static String regCate(String origin) {
|
static String regCate(String origin) {
|
||||||
Iterable<Match> matches = _exp.allMatches(origin);
|
Iterable<Match> matches = _exp.allMatches(origin);
|
||||||
@@ -17,14 +18,21 @@ abstract class Em {
|
|||||||
},
|
},
|
||||||
onNonMatch: (String str) {
|
onNonMatch: (String str) {
|
||||||
if (str != '') {
|
if (str != '') {
|
||||||
str = str
|
res.add((
|
||||||
.replaceAll('<', '<')
|
isEm: false,
|
||||||
.replaceAll('>', '>')
|
text: str.replaceAllMapped(
|
||||||
.replaceAll('"', '"')
|
_htmlRegExp,
|
||||||
.replaceAll(''', "'")
|
(m) => switch (m.group(1)) {
|
||||||
.replaceAll(' ', " ")
|
'lt' => '<',
|
||||||
.replaceAll('&', "&");
|
'gt' => '>',
|
||||||
res.add((isEm: false, text: str));
|
'quot' => '"',
|
||||||
|
'apos' => "'",
|
||||||
|
'nbsp' => ' ',
|
||||||
|
'amp' => '&',
|
||||||
|
_ => m.group(0)!,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -260,6 +260,14 @@ extension DirectoryExt on Directory {
|
|||||||
await delete(recursive: recursive);
|
await delete(recursive: recursive);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> lengthGte(int length) async {
|
||||||
|
int count = 0;
|
||||||
|
await for (var _ in list()) {
|
||||||
|
if (++count == length) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SizeExt on Size {
|
extension SizeExt on Size {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ abstract final class PathUtils {
|
|||||||
static const _fileExt = '.m4s';
|
static const _fileExt = '.m4s';
|
||||||
static const audioNameType2 = 'audio$_fileExt';
|
static const audioNameType2 = 'audio$_fileExt';
|
||||||
static const videoNameType2 = 'video$_fileExt';
|
static const videoNameType2 = 'video$_fileExt';
|
||||||
|
static const coverName = 'cover.jpg';
|
||||||
|
static const danmakuName = 'danmaku.pb';
|
||||||
static const downloadDir = 'download';
|
static const downloadDir = 'download';
|
||||||
|
|
||||||
static String buildShadersAbsolutePath(
|
static String buildShadersAbsolutePath(
|
||||||
|
|||||||
@@ -1996,7 +1996,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
xml:
|
xml:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||||
|
|||||||
@@ -220,7 +220,6 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/bggRGjQaUbCoE/super_sliver_list.git
|
url: https://github.com/bggRGjQaUbCoE/super_sliver_list.git
|
||||||
ref: mod
|
ref: mod
|
||||||
xml: ^6.6.1
|
|
||||||
dlna_dart: ^0.1.0
|
dlna_dart: ^0.1.0
|
||||||
|
|
||||||
vector_math: any
|
vector_math: any
|
||||||
|
|||||||
Reference in New Issue
Block a user