Skip to content

Commit 1b4f3aa

Browse files
authored
Add manual download fallback, logging system
Add manual download fallback, logging system, and fork attribution
2 parents f3ed727 + 18e2b55 commit 1b4f3aa

8 files changed

Lines changed: 363 additions & 30 deletions

File tree

lib/services/download_manager.dart

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:crypto/crypto.dart';
99
// Project imports:
1010
import 'package:openlib/services/database.dart' show MyLibraryDb, MyBook;
1111
import 'package:openlib/services/download_notification.dart';
12+
import 'package:openlib/services/logger.dart';
1213
import 'package:openlib/services/mirror_fetcher.dart';
1314

1415
enum DownloadStatus {
@@ -101,6 +102,7 @@ class DownloadManager {
101102
final MyLibraryDb _database = MyLibraryDb.instance;
102103
final DownloadNotificationService _notificationService =
103104
DownloadNotificationService();
105+
final AppLogger _logger = AppLogger();
104106

105107
final Map<String, DownloadTask> _activeDownloads = {};
106108
final StreamController<Map<String, DownloadTask>> _downloadsController =
@@ -113,6 +115,7 @@ class DownloadManager {
113115

114116
Future<void> initialize() async {
115117
await _notificationService.initialize();
118+
_logger.info('DownloadManager initialized', tag: 'DownloadManager');
116119
}
117120

118121
Future<String> _getFilePath(String fileName) async {
@@ -180,9 +183,11 @@ class DownloadManager {
180183

181184
Future<void> addDownload(DownloadTask task) async {
182185
if (_activeDownloads.containsKey(task.id)) {
186+
_logger.warning('Download already exists: ${task.title}', tag: 'DownloadManager');
183187
return;
184188
}
185189

190+
_logger.info('Adding download: ${task.title} (${task.format})', tag: 'DownloadManager');
186191
_activeDownloads[task.id] = task;
187192
_notifyListeners();
188193

@@ -197,9 +202,11 @@ class DownloadManager {
197202

198203
Future<void> addDownloadWithMirrorUrl(DownloadTask task, String mirrorUrl) async {
199204
if (_activeDownloads.containsKey(task.id)) {
205+
_logger.warning('Download already exists: ${task.title}', tag: 'DownloadManager');
200206
return;
201207
}
202208

209+
_logger.info('Adding download with mirror URL: ${task.title} (${task.format})', tag: 'DownloadManager');
203210
_activeDownloads[task.id] = task;
204211
_notifyListeners();
205212

@@ -412,6 +419,8 @@ class DownloadManager {
412419
Future<void> _startDownloadWithMirrorUrl(DownloadTask task, String mirrorUrl) async {
413420
Dio? dio;
414421
try {
422+
_logger.info('Starting download with mirror URL for: ${task.title}', tag: 'DownloadManager');
423+
415424
// Update status to fetching mirrors
416425
_updateTaskStatus(task.id, DownloadStatus.fetchingMirrors);
417426
await _notificationService.showDownloadNotification(
@@ -426,21 +435,26 @@ class DownloadManager {
426435
List<String> fetchedMirrors = await mirrorFetcher.fetchMirrors(mirrorUrl);
427436

428437
if (!_activeDownloads.containsKey(task.id)) {
438+
_logger.warning('Task cancelled while fetching mirrors: ${task.title}', tag: 'DownloadManager');
429439
return; // Task was cancelled while fetching mirrors
430440
}
431441

432442
if (fetchedMirrors.isEmpty) {
443+
_logger.error('Background mirror fetching failed for: ${task.title}', tag: 'DownloadManager');
444+
// Background fetching failed - store the mirror URL for fallback
433445
_updateTaskStatus(task.id, DownloadStatus.failed,
434-
errorMessage: 'Failed to fetch mirrors!');
446+
errorMessage: 'Manual verification required - please use "Download" button to open captcha page');
435447
await _notificationService.showDownloadNotification(
436448
id: task.id.hashCode,
437449
title: task.title,
438-
body: 'Failed: Could not get mirrors',
450+
body: 'Manual verification needed',
439451
progress: -1,
440452
);
441453
return;
442454
}
443455

456+
_logger.info('Successfully fetched ${fetchedMirrors.length} mirrors for: ${task.title}', tag: 'DownloadManager');
457+
444458
// Update task with fetched mirrors
445459
final updatedTask = task.copyWith(mirrors: fetchedMirrors);
446460
_activeDownloads[task.id] = updatedTask;

lib/services/logger.dart

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Dart imports:
2+
import 'dart:collection';
3+
import 'dart:io';
4+
5+
// Flutter imports:
6+
import 'package:flutter/foundation.dart';
7+
8+
// Package imports:
9+
import 'package:path_provider/path_provider.dart';
10+
import 'package:share_plus/share_plus.dart';
11+
12+
/// Log entry class to store individual log messages
13+
class LogEntry {
14+
final DateTime timestamp;
15+
final String level;
16+
final String message;
17+
final String? tag;
18+
final dynamic error;
19+
final StackTrace? stackTrace;
20+
21+
LogEntry({
22+
required this.timestamp,
23+
required this.level,
24+
required this.message,
25+
this.tag,
26+
this.error,
27+
this.stackTrace,
28+
});
29+
30+
@override
31+
String toString() {
32+
final buffer = StringBuffer();
33+
buffer.write('[${timestamp.toIso8601String()}] ');
34+
buffer.write('[$level] ');
35+
if (tag != null) {
36+
buffer.write('[$tag] ');
37+
}
38+
buffer.write(message);
39+
if (error != null) {
40+
buffer.write('\nError: $error');
41+
}
42+
if (stackTrace != null) {
43+
buffer.write('\nStack trace:\n$stackTrace');
44+
}
45+
return buffer.toString();
46+
}
47+
}
48+
49+
/// Logger service to capture and export app logs
50+
class AppLogger {
51+
static final AppLogger _instance = AppLogger._internal();
52+
factory AppLogger() => _instance;
53+
AppLogger._internal();
54+
55+
// Store logs for the past 5 minutes
56+
final Queue<LogEntry> _logs = Queue<LogEntry>();
57+
static const Duration _logRetentionDuration = Duration(minutes: 5);
58+
static const int _maxLogEntries = 1000; // Limit to prevent memory issues
59+
60+
/// Add a log entry
61+
void _addLog(String level, String message, {String? tag, dynamic error, StackTrace? stackTrace}) {
62+
final entry = LogEntry(
63+
timestamp: DateTime.now(),
64+
level: level,
65+
message: message,
66+
tag: tag,
67+
error: error,
68+
stackTrace: stackTrace,
69+
);
70+
71+
_logs.addLast(entry);
72+
73+
// Also print to console in debug mode
74+
if (kDebugMode) {
75+
debugPrint(entry.toString());
76+
}
77+
78+
// Remove old logs
79+
_cleanOldLogs();
80+
81+
// Limit log size
82+
while (_logs.length > _maxLogEntries) {
83+
_logs.removeFirst();
84+
}
85+
}
86+
87+
/// Remove logs older than 5 minutes
88+
void _cleanOldLogs() {
89+
final cutoffTime = DateTime.now().subtract(_logRetentionDuration);
90+
while (_logs.isNotEmpty && _logs.first.timestamp.isBefore(cutoffTime)) {
91+
_logs.removeFirst();
92+
}
93+
}
94+
95+
/// Log debug message
96+
void debug(String message, {String? tag}) {
97+
_addLog('DEBUG', message, tag: tag);
98+
}
99+
100+
/// Log info message
101+
void info(String message, {String? tag}) {
102+
_addLog('INFO', message, tag: tag);
103+
}
104+
105+
/// Log warning message
106+
void warning(String message, {String? tag, dynamic error}) {
107+
_addLog('WARNING', message, tag: tag, error: error);
108+
}
109+
110+
/// Log error message
111+
void error(String message, {String? tag, dynamic error, StackTrace? stackTrace}) {
112+
_addLog('ERROR', message, tag: tag, error: error, stackTrace: stackTrace);
113+
}
114+
115+
/// Get all logs as a formatted string
116+
String getAllLogs() {
117+
_cleanOldLogs();
118+
119+
final buffer = StringBuffer();
120+
buffer.writeln('=== Openlib App Logs ===');
121+
buffer.writeln('Generated: ${DateTime.now().toIso8601String()}');
122+
buffer.writeln('Log retention: Last ${_logRetentionDuration.inMinutes} minutes');
123+
buffer.writeln('Total entries: ${_logs.length}');
124+
buffer.writeln('');
125+
buffer.writeln('=== System Information ===');
126+
buffer.writeln('Platform: ${Platform.operatingSystem} ${Platform.operatingSystemVersion}');
127+
buffer.writeln('Dart version: ${Platform.version}');
128+
buffer.writeln('');
129+
buffer.writeln('=== Log Entries ===');
130+
131+
for (final log in _logs) {
132+
buffer.writeln(log.toString());
133+
buffer.writeln('');
134+
}
135+
136+
return buffer.toString();
137+
}
138+
139+
/// Export logs to a file and share
140+
Future<void> exportLogs() async {
141+
try {
142+
final logsContent = getAllLogs();
143+
144+
// Get temporary directory
145+
final tempDir = await getTemporaryDirectory();
146+
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-').replaceAll('.', '-');
147+
final file = File('${tempDir.path}/openlib_logs_$timestamp.txt');
148+
149+
// Write logs to file
150+
await file.writeAsString(logsContent);
151+
152+
// Share the file
153+
await Share.shareXFiles(
154+
[XFile(file.path)],
155+
subject: 'Openlib App Logs - $timestamp',
156+
text: 'Openlib app logs for the past ${_logRetentionDuration.inMinutes} minutes',
157+
);
158+
159+
info('Logs exported successfully', tag: 'AppLogger');
160+
} catch (e, stackTrace) {
161+
error('Failed to export logs', tag: 'AppLogger', error: e, stackTrace: stackTrace);
162+
rethrow;
163+
}
164+
}
165+
166+
/// Clear all logs
167+
void clearLogs() {
168+
_logs.clear();
169+
info('Logs cleared', tag: 'AppLogger');
170+
}
171+
172+
/// Get log count
173+
int get logCount {
174+
_cleanOldLogs();
175+
return _logs.length;
176+
}
177+
}

lib/services/mirror_fetcher.dart

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@ import 'dart:async';
44
// Package imports:
55
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
66

7+
// Project imports:
8+
import 'package:openlib/services/logger.dart';
9+
710
/// Service to fetch mirror links in the background without showing UI
811
class MirrorFetcherService {
912
static final MirrorFetcherService _instance = MirrorFetcherService._internal();
1013
factory MirrorFetcherService() => _instance;
1114
MirrorFetcherService._internal();
1215

16+
final AppLogger _logger = AppLogger();
17+
1318
/// Fetch mirror links from the given URL in the background
1419
/// Returns a list of mirror download links
1520
Future<List<String>> fetchMirrors(String url) async {
21+
_logger.info('Starting background mirror fetch from: $url', tag: 'MirrorFetcher');
22+
1623
final Completer<List<String>> completer = Completer<List<String>>();
1724
final List<String> bookDownloadLinks = [];
1825

@@ -22,12 +29,15 @@ class MirrorFetcherService {
2229
initialUrlRequest: URLRequest(url: WebUri(url)),
2330
onLoadStop: (controller, url) async {
2431
if (url == null) {
32+
_logger.warning('URL is null in onLoadStop', tag: 'MirrorFetcher');
2533
if (!completer.isCompleted) {
2634
completer.complete([]);
2735
}
2836
return;
2937
}
3038

39+
_logger.debug('Page loaded: ${url.toString()}', tag: 'MirrorFetcher');
40+
3141
try {
3242
if (url.toString().contains("slow_download")) {
3343
// For slow_download pages, extract the direct link
@@ -36,6 +46,7 @@ class MirrorFetcherService {
3646
String? mirrorLink = await controller.evaluateJavascript(source: query);
3747
if (mirrorLink != null) {
3848
bookDownloadLinks.add(mirrorLink);
49+
_logger.info('Extracted slow_download link', tag: 'MirrorFetcher');
3950
}
4051
} else {
4152
// For other mirror pages, extract all IPFS/mirror links
@@ -44,20 +55,23 @@ class MirrorFetcherService {
4455
List<dynamic> mirrorLinks =
4556
await controller.evaluateJavascript(source: query);
4657
bookDownloadLinks.addAll(mirrorLinks.cast<String>());
58+
_logger.info('Extracted ${bookDownloadLinks.length} mirror links', tag: 'MirrorFetcher');
4759
}
4860

4961
// Complete the future with the extracted links
5062
if (!completer.isCompleted) {
5163
completer.complete(bookDownloadLinks);
5264
}
5365
} catch (e) {
66+
_logger.error('JavaScript evaluation error', tag: 'MirrorFetcher', error: e);
5467
// Evaluation error, complete with empty list
5568
if (!completer.isCompleted) {
5669
completer.complete([]);
5770
}
5871
}
5972
},
6073
onReceivedError: (controller, request, error) {
74+
_logger.error('WebView error: ${error.description}', tag: 'MirrorFetcher', error: error);
6175
// Load error, complete with empty list
6276
if (!completer.isCompleted) {
6377
completer.complete([]);
@@ -67,19 +81,25 @@ class MirrorFetcherService {
6781

6882
// Run the headless webview
6983
await headlessWebView.run();
84+
_logger.debug('Headless webview started', tag: 'MirrorFetcher');
7085

7186
// Wait for the page to load and links to be extracted
7287
// Maximum wait time of 15 seconds
7388
final result = await completer.future.timeout(
7489
const Duration(seconds: 15),
75-
onTimeout: () => [],
90+
onTimeout: () {
91+
_logger.warning('Mirror fetch timed out after 15 seconds', tag: 'MirrorFetcher');
92+
return [];
93+
},
7694
);
7795

7896
// Dispose the headless webview
7997
await headlessWebView.dispose();
98+
_logger.debug('Headless webview disposed', tag: 'MirrorFetcher');
8099

81100
return result;
82-
} catch (e) {
101+
} catch (e, stackTrace) {
102+
_logger.error('Failed to fetch mirrors', tag: 'MirrorFetcher', error: e, stackTrace: stackTrace);
83103
// Return empty list on error
84104
return [];
85105
}

0 commit comments

Comments
 (0)