Skip to content

Commit 424227a

Browse files
authored
Merge pull request #1 from warreth/add-mirrors-and-settings
Add multi-instance support with automatic failover for Anna's Archive mirrors
2 parents 62c6181 + fe7d91a commit 424227a

6 files changed

Lines changed: 964 additions & 39 deletions

File tree

README.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@
22

33
<img src="assets/icons/appIcon.png" width="150">
44

5-
# Openlib
5+
# OpenlibExtended
6+
> I made this fork to keep using the app. It’s intended for personal use; I’ll keep it updated, but don’t expect weekly releases.
67
78
An Open source app to download and read books from shadow library ([Anna’s Archive](https://annas-archive.org/))
89

910
[![made-with-flutter](https://img.shields.io/badge/Made%20with-Flutter-4361ee.svg?style=for-the-badge)](https://flutter.dev/)
1011
[![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-e63946.svg?style=for-the-badge)](https://opensource.org/licenses/)
11-
[![Latest release](https://img.shields.io/github/release/dstark5/Openlib.svg?style=for-the-badge)](https://github.com/dstark5/Openlib/releases)
12+
[![Latest release](https://img.shields.io/github/release/warreth/OpenlibExtended.svg?style=for-the-badge)](https://github.com/warreth/OpenlibExtended/releases)
1213

1314
[<img src="github_releases.png"
1415
alt="Get it on GitHub"
15-
height="60">](https://github.com/dstark5/Openlib/releases)
16+
height="60">](https://github.com/warreth/OpenlibExtended/releases)
1617
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png"
1718
alt="Get it on IzzyOnDroid"
1819
height="60">](https://android.izzysoft.de/repo/apk/com.app.openlib)
@@ -28,7 +29,7 @@ An Open source app to download and read books from shadow library ([Anna’s Arc
2829

2930
**WARNING:** This App Is In Beta Stage, So You May Encounter Bugs. If You Do, Open An Issue In Github Repository.
3031

31-
**Publishing Openlib, Or Any Fork Of It In The Google Play Store Violates Their Terms And Conditions**
32+
**Publishing OpenlibExtended, Or Any Fork Of It In The Google Play Store Violates Their Terms And Conditions**
3233

3334
## Screenshots 🖼️
3435

@@ -43,12 +44,18 @@ An Open source app to download and read books from shadow library ([Anna’s Arc
4344

4445
## Description 📖
4546

46-
Openlib Is An Open Source App To Download And Read Books From Shadow Library ([Anna’s Archive](https://annas-archive.org/)). The App Has Built In Reader to Read Books
47+
OpenlibExtended Is An Open Source App To Download And Read Books From Shadow Library ([Anna’s Archive](https://annas-archive.org/)). The App Has Built In Reader to Read Books
4748

4849
As [Anna’s Archive](https://annas-archive.org/) Doesn't Have An API. The App Works By Sending Request To Anna’s Archive And Parses The Response To objects. The App Extracts The Mirrors From Response And Downloads The Book
4950

5051
## Features ✨
5152

53+
- **Multi-Instance Support** - Configure multiple Anna's Archive mirrors with automatic failover
54+
- 6 pre-configured instances (Anna's Archive .gs, .se, .li, .st, .pm + welib.org)
55+
- Add custom mirror instances
56+
- Drag-to-reorder priority
57+
- Enable/disable instances
58+
- Automatic retry (2x per instance) with seamless fallback
5259
- Trending Books
5360
- Download And Read Books With In-Built Viewer
5461
- Supports Epub And Pdf Formats
@@ -70,7 +77,7 @@ As [Anna’s Archive](https://annas-archive.org/) Doesn't Have An API. The App W
7077
- Git Clone The Repo
7178

7279
```sh
73-
git clone https://github.com/dstark5/Openlib.git
80+
git clone https://github.com/warreth/OpenlibExtended.git
7481
```
7582

7683
- Run the app with Android Studio or VS Code. Or the command line:
@@ -105,20 +112,20 @@ If you'd like to get involved See [CONTRIBUTING.md](./CONTRIBUTING.md) for the g
105112

106113
## Issues 🚩
107114

108-
Please report bugs via the [issue tracker](https://github.com/dstark5/Openlib/issues).
115+
Please report bugs via the [issue tracker](https://github.com/warreth/OpenlibExtended/issues).
109116

110117
## Donate 🎁
111118

112-
If you like Openlib, you're welcome to send a donation.
119+
If you like OpenlibExtended, you're welcome to send a donation.
113120
114121
[Donate To Anna’s Archive.](https://annas-archive.org/donate?tier=1)
115122
116123
## License 📜
117124
118125
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)
119126
120-
Openlib is a free software licensed under GPL v3.0 It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. [GNU General Public License](https://www.gnu.org/licenses/gpl.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
127+
OpenlibExtended is a free software licensed under GPL v3.0 It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. [GNU General Public License](https://www.gnu.org/licenses/gpl.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
121128
122129
## Disclaimer ⚠️
123130
124-
Openlib does not own or have any affiliation with the books available through the app. All books are the property of their respective owners and are protected by copyright law. Openlib is not responsible for any infringement of copyright or other intellectual property rights that may result from the use of the books available through the app. By using the app, you agree to use the books only for personal, non-commercial purposes and in compliance with all applicable laws and regulations.
131+
OpenlibExtended does not own or have any affiliation with the books available through the app. All books are the property of their respective owners and are protected by copyright law. OpenlibExtended is not responsible for any infringement of copyright or other intellectual property rights that may result from the use of the books available through the app. By using the app, you agree to use the books only for personal, non-commercial purposes and in compliance with all applicable laws and regulations.

lib/services/annas_archieve.dart

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import 'package:html/parser.dart' show parse;
77
import 'package:html/dom.dart' as dom;
88
import 'dart:convert';
99

10+
// Project imports:
11+
import 'package:openlib/services/instance_manager.dart';
12+
1013
// ====================================================================
1114
// DATA MODELS
1215
// ====================================================================
@@ -53,15 +56,58 @@ class BookInfoData extends BookData {
5356
// ====================================================================
5457

5558
class AnnasArchieve {
56-
static const String baseUrl = "https://annas-archive.org";
59+
static const String baseUrl = "https://annas-archive.org"; // Fallback default
5760

5861
final Dio dio = Dio();
62+
final InstanceManager _instanceManager = InstanceManager();
63+
static const int maxRetries = 2; // Check each server 2x as per requirements
64+
static const int retryDelayMs = 500; // Delay between retries in milliseconds
5965

6066
Map<String, dynamic> defaultDioHeaders = {
6167
"user-agent":
6268
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
6369
};
6470

71+
// Try request with retry logic across multiple instances
72+
Future<T> _requestWithRetry<T>(
73+
Future<T> Function(String baseUrl) requestFn,
74+
) async {
75+
final instances = await _instanceManager.getEnabledInstances();
76+
77+
if (instances.isEmpty) {
78+
// Use default if no instances are enabled
79+
return await requestFn(baseUrl);
80+
}
81+
82+
Exception? lastException;
83+
List<String> failedInstances = []; // Track failed instances for logging
84+
85+
// Try each instance
86+
for (final instance in instances) {
87+
// Try each instance up to maxRetries times
88+
for (int attempt = 0; attempt < maxRetries; attempt++) {
89+
try {
90+
return await requestFn(instance.baseUrl);
91+
} catch (e) {
92+
lastException = e is Exception ? e : Exception(e.toString());
93+
// Log the failure
94+
final attemptInfo = '${instance.name} (${instance.baseUrl}) - Attempt ${attempt + 1}/$maxRetries: ${e.toString()}';
95+
failedInstances.add(attemptInfo);
96+
print('Instance failed: $attemptInfo');
97+
98+
// If this is not the last attempt for this instance, wait before retrying
99+
if (attempt < maxRetries - 1) {
100+
await Future.delayed(Duration(milliseconds: retryDelayMs));
101+
}
102+
}
103+
}
104+
}
105+
106+
// If all instances failed, throw the last exception with context
107+
print('All instances failed. Attempted: ${failedInstances.join(", ")}');
108+
throw lastException ?? Exception('All instances failed');
109+
}
110+
65111
String getMd5(String url) {
66112
final uri = Uri.parse(url);
67113
final pathSegments = uri.pathSegments;
@@ -95,7 +141,7 @@ class AnnasArchieve {
95141
// --------------------------------------------------------------------
96142
// _parser FUNCTION (Search Results - Fixed nth-of-type issue)
97143
// --------------------------------------------------------------------
98-
List<BookData> _parser(resData, String fileType) {
144+
List<BookData> _parser(resData, String fileType, String currentBaseUrl) {
99145
var document = parse(resData.toString());
100146

101147
var bookContainers =
@@ -113,7 +159,7 @@ class AnnasArchieve {
113159
}
114160

115161
final String title = mainLinkElement.text.trim();
116-
final String link = baseUrl + mainLinkElement.attributes['href']!;
162+
final String link = currentBaseUrl + mainLinkElement.attributes['href']!;
117163
final String md5 = getMd5(mainLinkElement.attributes['href']!);
118164
final String? thumbnail = thumbnailElement?.attributes['src'];
119165

@@ -163,7 +209,7 @@ class AnnasArchieve {
163209
// --------------------------------------------------------------------
164210
// _bookInfoParser FUNCTION (Detail Page - Fixed 'unable to get data' error)
165211
// --------------------------------------------------------------------
166-
Future<BookInfoData?> _bookInfoParser(resData, url) async {
212+
Future<BookInfoData?> _bookInfoParser(resData, url, String currentBaseUrl) async {
167213
var document = parse(resData.toString());
168214
final main = document.querySelector('div.main-inner');
169215
if (main == null) return null;
@@ -172,7 +218,7 @@ class AnnasArchieve {
172218
String? mirror;
173219
final slowDownloadLinks = main.querySelectorAll('ul.list-inside a[href*="/slow_download/"]');
174220
if (slowDownloadLinks.isNotEmpty && slowDownloadLinks.first.attributes['href'] != null) {
175-
mirror = baseUrl + slowDownloadLinks.first.attributes['href']!;
221+
mirror = currentBaseUrl + slowDownloadLinks.first.attributes['href']!;
176222
}
177223
// --------------------------------
178224

@@ -239,12 +285,13 @@ class AnnasArchieve {
239285
required String content,
240286
required String sort,
241287
required String fileType,
242-
required bool enableFilters}) {
288+
required bool enableFilters,
289+
required String currentBaseUrl}) {
243290
searchQuery = searchQuery.replaceAll(" ", "+");
244291
if (!enableFilters) {
245-
return '$baseUrl/search?q=$searchQuery';
292+
return '$currentBaseUrl/search?q=$searchQuery';
246293
}
247-
return '$baseUrl/search?index=&q=$searchQuery&content=$content&ext=$fileType&sort=$sort';
294+
return '$currentBaseUrl/search?index=&q=$searchQuery&content=$content&ext=$fileType&sort=$sort';
248295
}
249296

250297
Future<List<BookData>> searchBooks(
@@ -254,16 +301,19 @@ class AnnasArchieve {
254301
String fileType = "",
255302
bool enableFilters = true}) async {
256303
try {
257-
final String encodedURL = urlEncoder(
258-
searchQuery: searchQuery,
259-
content: content,
260-
sort: sort,
261-
fileType: fileType,
262-
enableFilters: enableFilters);
263-
264-
final response = await dio.get(encodedURL,
265-
options: Options(headers: defaultDioHeaders));
266-
return _parser(response.data, fileType);
304+
return await _requestWithRetry<List<BookData>>((currentBaseUrl) async {
305+
final String encodedURL = urlEncoder(
306+
searchQuery: searchQuery,
307+
content: content,
308+
sort: sort,
309+
fileType: fileType,
310+
enableFilters: enableFilters,
311+
currentBaseUrl: currentBaseUrl);
312+
313+
final response = await dio.get(encodedURL,
314+
options: Options(headers: defaultDioHeaders));
315+
return _parser(response.data, fileType, currentBaseUrl);
316+
});
267317
} on DioException catch (e) {
268318
if (e.type == DioExceptionType.unknown) {
269319
throw "socketException";
@@ -274,16 +324,26 @@ class AnnasArchieve {
274324

275325
Future<BookInfoData> bookInfo({required String url}) async {
276326
try {
277-
final response =
278-
await dio.get(url, options: Options(headers: defaultDioHeaders));
279-
BookInfoData? data = await _bookInfoParser(response.data, url);
280-
if (data != null) {
281-
// Here's where you might use _safeParse if the API returned a numeric field
282-
// E.g., int pages = _safeParse(data.pages).toInt();
283-
return data;
284-
} else {
285-
throw 'unable to get data';
286-
}
327+
return await _requestWithRetry<BookInfoData>((currentBaseUrl) async {
328+
// Replace the base URL in the url parameter if it contains a different one
329+
String adjustedUrl = url;
330+
final urlParsed = Uri.parse(url);
331+
final currentParsed = Uri.parse(currentBaseUrl);
332+
333+
// If the URL has a different host, replace it with current instance's host
334+
if (urlParsed.host != currentParsed.host) {
335+
adjustedUrl = '$currentBaseUrl${urlParsed.path}${urlParsed.query.isNotEmpty ? "?${urlParsed.query}" : ""}';
336+
}
337+
338+
final response = await dio.get(adjustedUrl,
339+
options: Options(headers: defaultDioHeaders));
340+
BookInfoData? data = await _bookInfoParser(response.data, adjustedUrl, currentBaseUrl);
341+
if (data != null) {
342+
return data;
343+
} else {
344+
throw 'unable to get data';
345+
}
346+
});
287347
} on DioException catch (e) {
288348
if (e.type == DioExceptionType.unknown) {
289349
throw "socketException";

0 commit comments

Comments
 (0)