257 lines
8.5 KiB
Dart
257 lines
8.5 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:notube/models/video.dart';
|
|
import 'package:notube/services/converter.dart';
|
|
import 'package:notube/services/download.dart';
|
|
import 'package:notube/constants.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:downloadsfolder/downloadsfolder.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:notube/services/file_logger.dart';
|
|
|
|
part 'videos_state.dart';
|
|
|
|
class VideosCubit extends Cubit<VideosState> {
|
|
VideosCubit() : super(VideosState());
|
|
|
|
late DLServices dlService;
|
|
late ConverterService converterService;
|
|
|
|
Iterable<Video> downloadingVid = [];
|
|
|
|
int runningTasks = 0;
|
|
int maxConcurrentTasks = 3;
|
|
|
|
void addVideo(Video video) {
|
|
var nVideoList = state.videoList.toList();
|
|
nVideoList.add(video);
|
|
emit(VideosState(videoList: nVideoList));
|
|
}
|
|
|
|
Future<void> changeStatus(Video video, String status) async {
|
|
var completer = Completer<void>();
|
|
var tmpVideoList = state.videoList.toList();
|
|
int index = tmpVideoList.indexWhere((v) => v.id == video.id);
|
|
|
|
if (index != -1) {
|
|
tmpVideoList[index] = video.copyWith(status: status);
|
|
emit(VideosState(videoList: tmpVideoList));
|
|
completer.complete();
|
|
} else {
|
|
FileLogger().d('Video not found in the list');
|
|
completer.completeError('Video not found in the list');
|
|
}
|
|
return completer.future;
|
|
}
|
|
|
|
void setGlobalStatus(String status) {
|
|
emit(VideosState(status: status));
|
|
}
|
|
|
|
void clearVideos() {
|
|
emit(VideosState(videoList: []));
|
|
}
|
|
|
|
void removeVideo(Video video) {
|
|
var nVideoList = state.videoList.where((v) => v.id != video.id).toList();
|
|
emit(VideosState(videoList: nVideoList));
|
|
}
|
|
|
|
Future<void> archiveVideo(Video video) async {
|
|
FileLogger().d('Archiving video: ${video.title} ${video.format.format}');
|
|
var nVideoList = state.videoList.where((v) => v.id != video.id).toList();
|
|
var archivedVideos = state.archiveList.toList();
|
|
FileLogger().d('Archived videos before: ${archivedVideos.length}');
|
|
archivedVideos.add(video);
|
|
FileLogger().d('Archived videos after: $archivedVideos');
|
|
emit(state.copyWith(videoList: nVideoList, archiveList: archivedVideos));
|
|
}
|
|
|
|
Future<void> startConverter() async {
|
|
var videosToConvert = state.videoList.where((video) =>
|
|
video.status == 'downloaded' &&
|
|
convertedFormats.contains(video.format));
|
|
for (Video video in videosToConvert) {
|
|
await convertVideo(video);
|
|
}
|
|
FileLogger().d('All videos converted');
|
|
FileLogger().d('All videos converted');
|
|
}
|
|
|
|
Future<void> startDownloader() async {
|
|
dlService = await DLServices.init();
|
|
|
|
downloadingVid =
|
|
state.videoList.where((video) => video.status == 'pending').take(3);
|
|
|
|
while (downloadingVid.isNotEmpty && runningTasks < maxConcurrentTasks) {
|
|
FileLogger().d('Videos to download: ${downloadingVid.length}');
|
|
runningTasks++;
|
|
FileLogger().d('Concurrent workers: $runningTasks');
|
|
|
|
await downloadVideo(downloadingVid.first);
|
|
downloadingVid =
|
|
state.videoList.where((video) => video.status == 'pending').take(3);
|
|
}
|
|
|
|
await startConverter();
|
|
|
|
await moveVideos();
|
|
|
|
final Directory directory = Directory('temp');
|
|
final List<FileSystemEntity> files = directory.listSync();
|
|
for (FileSystemEntity file in files) {
|
|
final String dirFilename = p.basenameWithoutExtension(file.path);
|
|
if (dirFilename.contains('_tmp')) {
|
|
file.delete();
|
|
}
|
|
}
|
|
|
|
FileLogger().d('All videos downloaded');
|
|
FileLogger().d('All videos downloaded');
|
|
}
|
|
|
|
Future moveVideos() async {
|
|
FileLogger().d('Moving videos to download folder');
|
|
final prefs = await SharedPreferences.getInstance();
|
|
Directory defaultDownloadDirectory = await getDownloadDirectory();
|
|
prefs.get('downloadFolder') ??
|
|
await prefs.setString('downloadFolder', defaultDownloadDirectory.path);
|
|
|
|
final downloadDirectory = Directory(
|
|
prefs.getString('downloadFolder') ?? defaultDownloadDirectory.path);
|
|
|
|
for (Video video in state.videoList.where((video) =>
|
|
video.status == 'downloaded' || video.status == 'converted')) {
|
|
var cleanTitle = video.title.replaceAll(RegExp(r'[^\w\s]+'), '');
|
|
final tmpFile =
|
|
File('temp/${video.filename}_done.${video.format.extension}');
|
|
|
|
var playlistTitle = video.playlistTitle;
|
|
await Directory('${downloadDirectory.path}/$playlistTitle')
|
|
.create(recursive: true)
|
|
.catchError((e) => FileLogger().e('Error creating directory: $e'));
|
|
FileLogger().d('Playlist title: $playlistTitle');
|
|
if (playlistTitle.isNotEmpty && playlistTitle != '') {
|
|
cleanTitle = '$playlistTitle/$cleanTitle';
|
|
}
|
|
|
|
final newFile = File(
|
|
'${downloadDirectory.path}/$cleanTitle.${video.format.extension}');
|
|
|
|
var isMoved = false;
|
|
while (!isMoved) {
|
|
try {
|
|
await tmpFile.rename(newFile.path);
|
|
isMoved = true;
|
|
} on FileSystemException catch (e) {
|
|
FileLogger().e('Error moving file: $e');
|
|
await Future.delayed(Duration(seconds: 1));
|
|
}
|
|
}
|
|
await changeStatus(video, 'done');
|
|
await archiveVideo(video.copyWith(status: 'done'));
|
|
FileLogger().d('File moved to ${newFile.path}');
|
|
}
|
|
}
|
|
|
|
Future convertVideo(Video video) async {
|
|
converterService = await ConverterService.init();
|
|
FileLogger().d('Converting ${video.title} to ${video.format.format}');
|
|
FileLogger().d('Converting ${video.title} to ${video.format.format}');
|
|
changeStatus(video, 'converting');
|
|
final shellStream = await converterService.convertFile(video);
|
|
var duration = '0.0';
|
|
var completer = Completer<void>();
|
|
|
|
shellStream.listen(
|
|
(line) {
|
|
FileLogger().d(line);
|
|
//FFmpeg doesn't return any output for audio conversion
|
|
if (video.format.extension == "mp3") {
|
|
changeStatus(video, 'converted');
|
|
if (!completer.isCompleted) {
|
|
FileLogger().d('____Conversion audio completed___');
|
|
FileLogger().d('Conversion audio completed');
|
|
runningTasks--;
|
|
completer.complete();
|
|
}
|
|
} else {
|
|
if (line.contains('Duration: ')) {
|
|
var durationString =
|
|
line.split('Duration: ')[1].split(',')[0].trim();
|
|
duration = durationString;
|
|
FileLogger().d('Duration: $duration');
|
|
}
|
|
if (line.contains('out_time_ms=')) {
|
|
var timeString = line.split('out_time_ms=')[1];
|
|
var time = timeString;
|
|
FileLogger().d('Time: $time');
|
|
changeStatus(video, 'converting $time on $duration');
|
|
}
|
|
if (line.contains('progress=end')) {
|
|
changeStatus(video, 'converted');
|
|
if (!completer.isCompleted) {
|
|
FileLogger().d('____Conversion completed___');
|
|
runningTasks--;
|
|
completer.complete();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
onError: (error) {
|
|
if (!completer.isCompleted) {
|
|
completer
|
|
.completeError(error); // Complete with error if an error occurs
|
|
}
|
|
},
|
|
);
|
|
return completer.future;
|
|
}
|
|
|
|
Future downloadVideo(Video video) async {
|
|
FileLogger().d(
|
|
'___Downloading ${video.title} in ${video.format} format ${video.filename}____');
|
|
|
|
FileLogger().d(
|
|
'___Downloading ${video.title} in ${video.format} format ${video.filename}____');
|
|
final shellStream = await dlService.downloadFile(video);
|
|
var completer = Completer<void>();
|
|
|
|
shellStream.listen(
|
|
(line) {
|
|
if (line.contains('[download]') &&
|
|
line.contains('of') &&
|
|
line.contains('%')) {
|
|
var percentString =
|
|
line.split('[download]')[1].split(' of')[0].split('%')[0].trim();
|
|
var percent = percentString == null ? 0 : double.parse(percentString);
|
|
changeStatus(video, 'downloading $percent%');
|
|
}
|
|
|
|
if (line.contains('[EmbedThumbnail]')) {
|
|
changeStatus(video, 'downloaded');
|
|
|
|
if (!completer.isCompleted) {
|
|
FileLogger().d('____Download completed___');
|
|
FileLogger().d('Download completed');
|
|
runningTasks--;
|
|
completer.complete();
|
|
}
|
|
}
|
|
//FileLogger().d(line);
|
|
},
|
|
onError: (error) {
|
|
if (!completer.isCompleted) {
|
|
completer
|
|
.completeError(error); // Complete with error if an error occurs
|
|
}
|
|
},
|
|
);
|
|
return completer.future;
|
|
}
|
|
}
|