APKPure, SourceHut, Bugfixes

This commit is contained in:
Imran Remtulla
2023-05-05 22:35:32 -04:00
parent 84b512f282
commit fb9e66332d
23 changed files with 445 additions and 122 deletions

View File

@ -0,0 +1,116 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
class APKCombo extends AppSource {
APKCombo() {
host = 'apkcombo.com';
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://$host/+[^/]+/+[^/]+');
var match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
}
@override
String? tryInferringAppId(String standardUrl,
{Map<String, dynamic> additionalSettings = const {}}) {
return Uri.parse(standardUrl).pathSegments.last;
}
@override
Map<String, String> get requestHeaders => {
"User-Agent": "curl/8.0.1",
"Accept": "*/*",
"Connection": "keep-alive",
"Host": "$host"
};
Future<List<MapEntry<String, String>>> getApkUrls(String standardUrl) async {
var res = await sourceRequest('$standardUrl/download/apk');
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}
var html = parse(res.body);
return html
.querySelectorAll('#variants-tab > div > ul > li')
.map((e) {
String? arch = e
.querySelector('code')
?.text
.trim()
.replaceAll(',', '')
.replaceAll(':', '-')
.replaceAll(' ', '-');
return e.querySelectorAll('a').map((e) {
String? url = e.attributes['href'];
if (url != null &&
!Uri.parse(url).path.toLowerCase().endsWith('.apk')) {
url = null;
}
String verCode =
e.querySelector('.info .header .vercode')?.text.trim() ?? '';
return MapEntry<String, String>(
arch != null ? '$arch-$verCode.apk' : '', url ?? '');
}).toList();
})
.reduce((value, element) => [...value, ...element])
.where((element) => element.value.isNotEmpty)
.toList();
}
@override
Future<String> apkUrlPrefetchModifier(
String apkUrl, String standardUrl) async {
var freshURLs = await getApkUrls(standardUrl);
var path2Match = Uri.parse(apkUrl).path;
for (var url in freshURLs) {
if (Uri.parse(url.value).path == path2Match) {
return url.value;
}
}
throw NoAPKError();
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
String appId = tryInferringAppId(standardUrl)!;
String host = Uri.parse(standardUrl).host;
var preres = await sourceRequest(standardUrl);
if (preres.statusCode != 200) {
throw getObtainiumHttpError(preres);
}
var res = parse(preres.body);
String? version = res.querySelector('div.version')?.text.trim();
if (version == null) {
throw NoVersionError();
}
String appName = res.querySelector('div.app_name')?.text.trim() ?? appId;
String author = res.querySelector('div.author')?.text.trim() ?? appName;
List<String> infoArray = res
.querySelectorAll('div.information-table > .item > div.value')
.map((e) => e.text.trim())
.toList();
DateTime? releaseDate;
if (infoArray.length >= 2) {
try {
releaseDate = DateFormat('MMMM d, yyyy').parse(infoArray[1]);
} catch (e) {
// ignore
}
}
return APKDetails(
version, await getApkUrls(standardUrl), AppNames(author, appName),
releaseDate: releaseDate);
}
}

View File

@ -57,7 +57,7 @@ class APKMirror extends AppSource {
true true
? additionalSettings['filterReleaseTitlesByRegEx'] ? additionalSettings['filterReleaseTitlesByRegEx']
: null; : null;
Response res = await get(Uri.parse('$standardUrl/feed')); Response res = await sourceRequest('$standardUrl/feed');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var items = parse(res.body).querySelectorAll('item'); var items = parse(res.body).querySelectorAll('item');
dynamic targetRelease; dynamic targetRelease;

View File

@ -0,0 +1,69 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
class APKPure extends AppSource {
APKPure() {
host = 'apkpure.com';
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegExB = RegExp('^https?://m.$host/+[^/]+/+[^/]+');
RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
if (match != null) {
url = 'https://$host/${Uri.parse(url).path}';
}
RegExp standardUrlRegExA = RegExp('^https?://$host/+[^/]+/+[^/]+');
match = standardUrlRegExA.firstMatch(url.toLowerCase());
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
}
@override
String? tryInferringAppId(String standardUrl,
{Map<String, dynamic> additionalSettings = const {}}) {
return Uri.parse(standardUrl).pathSegments.last;
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
String appId = tryInferringAppId(standardUrl)!;
String host = Uri.parse(standardUrl).host;
var res = await sourceRequest('$standardUrl/download');
if (res.statusCode == 200) {
var html = parse(res.body);
String? version = html.querySelector('span.info-sdk span')?.text.trim();
if (version == null) {
throw NoVersionError();
}
String? dateString =
html.querySelector('span.info-other span.date')?.text.trim();
DateTime? releaseDate = dateString != null
? DateFormat('MMMM d, yyyy').parse(dateString)
: null;
List<MapEntry<String, String>> apkUrls = [
MapEntry('$appId.apk', 'https://d.$host/b/APK/$appId?version=latest')
];
String author = html
.querySelector('span.info-sdk')
?.text
.trim()
.substring(version.length + 4) ??
Uri.parse(standardUrl).pathSegments.reversed.last;
String appName =
html.querySelector('h1.info-title')?.text.trim() ?? appId;
return APKDetails(version, apkUrls, AppNames(author, appName),
releaseDate: releaseDate);
} else {
throw getObtainiumHttpError(res);
}
}
}

View File

@ -9,6 +9,7 @@ import 'package:obtainium/providers/source_provider.dart';
class Codeberg extends AppSource { class Codeberg extends AppSource {
Codeberg() { Codeberg() {
host = 'codeberg.org'; host = 'codeberg.org';
overrideEligible = true;
additionalSourceSpecificSettingFormItems = []; additionalSourceSpecificSettingFormItems = [];

View File

@ -66,14 +66,14 @@ class FDroid extends AppSource {
String? appId = tryInferringAppId(standardUrl); String? appId = tryInferringAppId(standardUrl);
String host = Uri.parse(standardUrl).host; String host = Uri.parse(standardUrl).host;
return getAPKUrlsFromFDroidPackagesAPIResponse( return getAPKUrlsFromFDroidPackagesAPIResponse(
await get(Uri.parse('https://$host/api/v1/packages/$appId')), await sourceRequest('https://$host/api/v1/packages/$appId'),
'https://$host/repo/$appId', 'https://$host/repo/$appId',
standardUrl); standardUrl);
} }
@override @override
Future<Map<String, List<String>>> search(String query) async { Future<Map<String, List<String>>> search(String query) async {
Response res = await get(Uri.parse('https://search.$host/?q=$query')); Response res = await sourceRequest('https://search.$host/?q=$query');
if (res.statusCode == 200) { if (res.statusCode == 200) {
Map<String, List<String>> urlsWithDescriptions = {}; Map<String, List<String>> urlsWithDescriptions = {};
parse(res.body).querySelectorAll('.package-header').forEach((e) { parse(res.body).querySelectorAll('.package-header').forEach((e) {

View File

@ -8,6 +8,7 @@ import 'package:obtainium/providers/source_provider.dart';
class FDroidRepo extends AppSource { class FDroidRepo extends AppSource {
FDroidRepo() { FDroidRepo() {
name = tr('fdroidThirdPartyRepo'); name = tr('fdroidThirdPartyRepo');
overrideEligible = true;
additionalSourceAppSpecificSettingFormItems = [ additionalSourceAppSpecificSettingFormItems = [
[ [
@ -28,7 +29,7 @@ class FDroidRepo extends AppSource {
if (appIdOrName == null) { if (appIdOrName == null) {
throw NoReleasesError(); throw NoReleasesError();
} }
var res = await get(Uri.parse('$standardUrl/index.xml')); var res = await sourceRequest('$standardUrl/index.xml');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var body = parse(res.body); var body = parse(res.body);
var foundApps = body.querySelectorAll('application').where((element) { var foundApps = body.querySelectorAll('application').where((element) {

View File

@ -11,6 +11,7 @@ import 'package:url_launcher/url_launcher_string.dart';
class GitHub extends AppSource { class GitHub extends AppSource {
GitHub() { GitHub() {
host = 'github.com'; host = 'github.com';
overrideEligible = true;
additionalSourceSpecificSettingFormItems = [ additionalSourceSpecificSettingFormItems = [
GeneratedFormTextField('github-creds', GeneratedFormTextField('github-creds',
@ -108,7 +109,7 @@ class GitHub extends AppSource {
true true
? additionalSettings['filterReleaseTitlesByRegEx'] ? additionalSettings['filterReleaseTitlesByRegEx']
: null; : null;
Response res = await get(Uri.parse(requestUrl)); Response res = await sourceRequest(requestUrl);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var releases = jsonDecode(res.body) as List<dynamic>; var releases = jsonDecode(res.body) as List<dynamic>;
@ -216,7 +217,7 @@ class GitHub extends AppSource {
Future<Map<String, List<String>>> searchCommon( Future<Map<String, List<String>>> searchCommon(
String query, String requestUrl, String rootProp, String query, String requestUrl, String rootProp,
{Function(Response)? onHttpErrorCode}) async { {Function(Response)? onHttpErrorCode}) async {
Response res = await get(Uri.parse(requestUrl)); Response res = await sourceRequest(requestUrl);
if (res.statusCode == 200) { if (res.statusCode == 200) {
Map<String, List<String>> urlsWithDescriptions = {}; Map<String, List<String>> urlsWithDescriptions = {};
for (var e in (jsonDecode(res.body)[rootProp] as List<dynamic>)) { for (var e in (jsonDecode(res.body)[rootProp] as List<dynamic>)) {

View File

@ -9,6 +9,7 @@ import 'package:easy_localization/easy_localization.dart';
class GitLab extends AppSource { class GitLab extends AppSource {
GitLab() { GitLab() {
host = 'gitlab.com'; host = 'gitlab.com';
overrideEligible = true;
additionalSourceAppSpecificSettingFormItems = [ additionalSourceAppSpecificSettingFormItems = [
[ [
@ -39,7 +40,7 @@ class GitLab extends AppSource {
) async { ) async {
bool fallbackToOlderReleases = bool fallbackToOlderReleases =
additionalSettings['fallbackToOlderReleases'] == true; additionalSettings['fallbackToOlderReleases'] == true;
Response res = await get(Uri.parse('$standardUrl/-/tags?format=atom')); Response res = await sourceRequest('$standardUrl/-/tags?format=atom');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var standardUri = Uri.parse(standardUrl); var standardUri = Uri.parse(standardUrl);
var parsedHtml = parse(res.body); var parsedHtml = parse(res.body);

View File

@ -4,84 +4,109 @@ import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
try {
Uri.parse(ambiguousUrl).origin;
return ambiguousUrl;
} catch (err) {
// is relative
}
var currPathSegments = referenceAbsoluteUrl.path
.split('/')
.where((element) => element.trim().isNotEmpty)
.toList();
if (ambiguousUrl.startsWith('/') || currPathSegments.isEmpty) {
return '${referenceAbsoluteUrl.origin}/$ambiguousUrl';
} else if (ambiguousUrl.split('/').length == 1) {
return '${referenceAbsoluteUrl.origin}/${currPathSegments.join('/')}/$ambiguousUrl';
} else {
return '${referenceAbsoluteUrl.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$ambiguousUrl';
}
}
int compareAlphaNumeric(String a, String b) {
List<String> aParts = _splitAlphaNumeric(a);
List<String> bParts = _splitAlphaNumeric(b);
for (int i = 0; i < aParts.length && i < bParts.length; i++) {
String aPart = aParts[i];
String bPart = bParts[i];
bool aIsNumber = _isNumeric(aPart);
bool bIsNumber = _isNumeric(bPart);
if (aIsNumber && bIsNumber) {
int aNumber = int.parse(aPart);
int bNumber = int.parse(bPart);
int cmp = aNumber.compareTo(bNumber);
if (cmp != 0) {
return cmp;
}
} else if (!aIsNumber && !bIsNumber) {
int cmp = aPart.compareTo(bPart);
if (cmp != 0) {
return cmp;
}
} else {
// Alphanumeric strings come before numeric strings
return aIsNumber ? 1 : -1;
}
}
return aParts.length.compareTo(bParts.length);
}
List<String> _splitAlphaNumeric(String s) {
List<String> parts = [];
StringBuffer sb = StringBuffer();
bool isNumeric = _isNumeric(s[0]);
sb.write(s[0]);
for (int i = 1; i < s.length; i++) {
bool currentIsNumeric = _isNumeric(s[i]);
if (currentIsNumeric == isNumeric) {
sb.write(s[i]);
} else {
parts.add(sb.toString());
sb.clear();
sb.write(s[i]);
isNumeric = currentIsNumeric;
}
}
parts.add(sb.toString());
return parts;
}
bool _isNumeric(String s) {
return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57;
}
class HTML extends AppSource { class HTML extends AppSource {
HTML() {
overrideEligible = true;
}
@override @override
String sourceSpecificStandardizeURL(String url) { String sourceSpecificStandardizeURL(String url) {
return url; return url;
} }
int compareAlphaNumeric(String a, String b) {
List<String> aParts = _splitAlphaNumeric(a);
List<String> bParts = _splitAlphaNumeric(b);
for (int i = 0; i < aParts.length && i < bParts.length; i++) {
String aPart = aParts[i];
String bPart = bParts[i];
bool aIsNumber = _isNumeric(aPart);
bool bIsNumber = _isNumeric(bPart);
if (aIsNumber && bIsNumber) {
int aNumber = int.parse(aPart);
int bNumber = int.parse(bPart);
int cmp = aNumber.compareTo(bNumber);
if (cmp != 0) {
return cmp;
}
} else if (!aIsNumber && !bIsNumber) {
int cmp = aPart.compareTo(bPart);
if (cmp != 0) {
return cmp;
}
} else {
// Alphanumeric strings come before numeric strings
return aIsNumber ? 1 : -1;
}
}
return aParts.length.compareTo(bParts.length);
}
List<String> _splitAlphaNumeric(String s) {
List<String> parts = [];
StringBuffer sb = StringBuffer();
bool isNumeric = _isNumeric(s[0]);
sb.write(s[0]);
for (int i = 1; i < s.length; i++) {
bool currentIsNumeric = _isNumeric(s[i]);
if (currentIsNumeric == isNumeric) {
sb.write(s[i]);
} else {
parts.add(sb.toString());
sb.clear();
sb.write(s[i]);
isNumeric = currentIsNumeric;
}
}
parts.add(sb.toString());
return parts;
}
bool _isNumeric(String s) {
return s.codeUnitAt(0) >= 48 && s.codeUnitAt(0) <= 57;
}
@override @override
Future<APKDetails> getLatestAPKDetails( Future<APKDetails> getLatestAPKDetails(
String standardUrl, String standardUrl,
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
var uri = Uri.parse(standardUrl); var uri = Uri.parse(standardUrl);
Response res = await get(uri); Response res = await sourceRequest(standardUrl);
if (res.statusCode == 200) { if (res.statusCode == 200) {
List<String> links = parse(res.body) List<String> links = parse(res.body)
.querySelectorAll('a') .querySelectorAll('a')
.map((element) => element.attributes['href'] ?? '') .map((element) => element.attributes['href'] ?? '')
.where((element) => element.toLowerCase().endsWith('.apk')) .where((element) =>
Uri.parse(element).path.toLowerCase().endsWith('.apk'))
.toList(); .toList();
links.sort( links.sort(
(a, b) => compareAlphaNumeric(a.split('/').last, b.split('/').last)); (a, b) => compareAlphaNumeric(a.split('/').last, b.split('/').last));
@ -95,25 +120,8 @@ class HTML extends AppSource {
var rel = links.last; var rel = links.last;
var apkName = rel.split('/').last; var apkName = rel.split('/').last;
var version = apkName.substring(0, apkName.length - 4); var version = apkName.substring(0, apkName.length - 4);
List<String> apkUrls = [rel].map((e) { List<String> apkUrls =
try { [rel].map((e) => ensureAbsoluteUrl(e, uri)).toList();
Uri.parse(e).origin;
return e;
} catch (err) {
// is relative
}
var currPathSegments = uri.path
.split('/')
.where((element) => element.trim().isNotEmpty)
.toList();
if (e.startsWith('/') || currPathSegments.isEmpty) {
return '${uri.origin}/$e';
} else if (e.split('/').length == 1) {
return '${uri.origin}/${currPathSegments.join('/')}/$e';
} else {
return '${uri.origin}/${currPathSegments.sublist(0, currPathSegments.length - 1).join('/')}/$e';
}
}).toList();
return APKDetails( return APKDetails(
version, getApkUrlsFromUrls(apkUrls), AppNames(uri.host, tr('app'))); version, getApkUrlsFromUrls(apkUrls), AppNames(uri.host, tr('app')));
} else { } else {

View File

@ -31,8 +31,8 @@ class IzzyOnDroid extends AppSource {
) async { ) async {
String? appId = tryInferringAppId(standardUrl); String? appId = tryInferringAppId(standardUrl);
return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse( return FDroid().getAPKUrlsFromFDroidPackagesAPIResponse(
await get( await sourceRequest(
Uri.parse('https://apt.izzysoft.de/fdroid/api/v1/packages/$appId')), 'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
'https://android.izzysoft.de/frepo/$appId', 'https://android.izzysoft.de/frepo/$appId',
standardUrl); standardUrl);
} }

View File

@ -6,6 +6,7 @@ import 'package:obtainium/providers/source_provider.dart';
class Jenkins extends AppSource { class Jenkins extends AppSource {
Jenkins() { Jenkins() {
overrideEligible = true;
overrideVersionDetectionFormDefault('releaseDateAsVersion', true); overrideVersionDetectionFormDefault('releaseDateAsVersion', true);
} }
@ -30,7 +31,7 @@ class Jenkins extends AppSource {
) async { ) async {
standardUrl = trimJobUrl(standardUrl); standardUrl = trimJobUrl(standardUrl);
Response res = Response res =
await get(Uri.parse('$standardUrl/lastSuccessfulBuild/api/json')); await sourceRequest('$standardUrl/lastSuccessfulBuild/api/json');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var json = jsonDecode(res.body); var json = jsonDecode(res.body);
var releaseDate = json['timestamp'] == null var releaseDate = json['timestamp'] == null
@ -55,9 +56,6 @@ class Jenkins extends AppSource {
.where((url) => .where((url) =>
url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk')) url.value.isNotEmpty && url.key.toLowerCase().endsWith('.apk'))
.toList(); .toList();
if (apkUrls.isEmpty) {
throw NoAPKError();
}
return APKDetails( return APKDetails(
version, version,
apkUrls, apkUrls,

View File

@ -28,7 +28,7 @@ class Mullvad extends AppSource {
String standardUrl, String standardUrl,
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
Response res = await get(Uri.parse('$standardUrl/en/download/android')); Response res = await sourceRequest('$standardUrl/en/download/android');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var versions = parse(res.body) var versions = parse(res.body)
.querySelectorAll('p') .querySelectorAll('p')

View File

@ -78,7 +78,7 @@ class NeutronCode extends AppSource {
String standardUrl, String standardUrl,
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
Response res = await get(Uri.parse(standardUrl)); Response res = await sourceRequest(standardUrl);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var http = parse(res.body); var http = parse(res.body);
var name = http.querySelector('.pd-title')?.innerHtml; var name = http.querySelector('.pd-title')?.innerHtml;

View File

@ -19,7 +19,7 @@ class Signal extends AppSource {
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
Response res = Response res =
await get(Uri.parse('https://updates.$host/android/latest.json')); await sourceRequest('https://updates.$host/android/latest.json');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var json = jsonDecode(res.body); var json = jsonDecode(res.body);
String? apkUrl = json['url']; String? apkUrl = json['url'];

View File

@ -6,6 +6,7 @@ import 'package:obtainium/providers/source_provider.dart';
class SourceForge extends AppSource { class SourceForge extends AppSource {
SourceForge() { SourceForge() {
host = 'sourceforge.net'; host = 'sourceforge.net';
overrideEligible = true;
} }
@override @override
@ -29,7 +30,7 @@ class SourceForge extends AppSource {
String standardUrl, String standardUrl,
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
Response res = await get(Uri.parse('$standardUrl/rss?path=/')); Response res = await sourceRequest('$standardUrl/rss?path=/');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var parsedHtml = parse(res.body); var parsedHtml = parse(res.body);
var allDownloadLinks = var allDownloadLinks =

View File

@ -0,0 +1,101 @@
import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/app_sources/html.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart';
import 'package:obtainium/components/generated_form.dart';
import 'package:easy_localization/easy_localization.dart';
class SourceHut extends AppSource {
SourceHut() {
host = 'git.sr.ht';
overrideEligible = true;
additionalSourceAppSpecificSettingFormItems = [
[
GeneratedFormSwitch('fallbackToOlderReleases',
label: tr('fallbackToOlderReleases'), defaultValue: true)
]
];
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
}
@override
String? changeLogPageFromStandardUrl(String standardUrl) => standardUrl;
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
Uri standardUri = Uri.parse(standardUrl);
String appName = standardUri.pathSegments.last;
bool fallbackToOlderReleases =
additionalSettings['fallbackToOlderReleases'] == true;
Response res = await sourceRequest('$standardUrl/refs/rss.xml');
if (res.statusCode == 200) {
var parsedHtml = parse(res.body);
List<APKDetails> apkDetailsList = [];
int ind = 0;
for (var entry in parsedHtml.querySelectorAll('item').sublist(0, 6)) {
// Limit 5 for speed
if (!fallbackToOlderReleases && ind > 0) {
break;
}
String? version = entry.querySelector('title')?.text.trim();
if (version == null) {
throw NoVersionError();
}
String? releaseDateString = entry.querySelector('pubDate')?.innerHtml;
var link = entry.querySelector('link');
String releasePage = '$standardUrl/refs/$version';
DateTime? releaseDate = releaseDateString != null
? DateFormat('EEE, dd MMM yyyy HH:mm:ss Z').parse(releaseDateString)
: null;
var res2 = await sourceRequest(releasePage);
List<MapEntry<String, String>> apkUrls = [];
if (res2.statusCode == 200) {
apkUrls = getApkUrlsFromUrls(parse(res2.body)
.querySelectorAll('a')
.map((e) => e.attributes['href'] ?? '')
.where((e) => e.toLowerCase().endsWith('.apk'))
.map((e) => ensureAbsoluteUrl(e, standardUri))
.toList());
}
apkDetailsList.add(APKDetails(
version,
apkUrls,
AppNames(entry.querySelector('author')?.innerHtml.trim() ?? appName,
appName),
releaseDate: releaseDate));
ind++;
}
if (apkDetailsList.isEmpty) {
throw NoReleasesError();
}
if (fallbackToOlderReleases) {
if (additionalSettings['trackOnly'] != true) {
apkDetailsList =
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
}
if (apkDetailsList.isEmpty) {
throw NoReleasesError();
}
}
return apkDetailsList.first;
} else {
throw getObtainiumHttpError(res);
}
}
}

View File

@ -29,7 +29,7 @@ class SteamMobile extends AppSource {
String standardUrl, String standardUrl,
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
Response res = await get(Uri.parse('https://$host/mobile')); Response res = await sourceRequest('https://$host/mobile');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var apkNamePrefix = additionalSettings['app'] as String?; var apkNamePrefix = additionalSettings['app'] as String?;
if (apkNamePrefix == null) { if (apkNamePrefix == null) {

View File

@ -20,7 +20,7 @@ class TelegramApp extends AppSource {
String standardUrl, String standardUrl,
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
Response res = await get(Uri.parse('https://t.me/s/TAndroidAPK')); Response res = await sourceRequest('https://t.me/s/TAndroidAPK');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var http = parse(res.body); var http = parse(res.body);
var messages = var messages =

View File

@ -19,8 +19,8 @@ class VLC extends AppSource {
String standardUrl, String standardUrl,
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
Response res = await get( Response res = await sourceRequest(
Uri.parse('https://www.videolan.org/vlc/download-android.html')); 'https://www.videolan.org/vlc/download-android.html');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var dwUrlBase = 'get.videolan.org/vlc-android'; var dwUrlBase = 'get.videolan.org/vlc-android';
var dwLinks = parse(res.body) var dwLinks = parse(res.body)
@ -38,7 +38,7 @@ class VLC extends AppSource {
throw NoVersionError(); throw NoVersionError();
} }
String? targetUrl = 'https://$dwUrlBase/$version/'; String? targetUrl = 'https://$dwUrlBase/$version/';
Response res2 = await get(Uri.parse(targetUrl)); Response res2 = await sourceRequest(targetUrl);
String mirrorDwBase = String mirrorDwBase =
'https://plug-mirror.rcac.purdue.edu/vlc/vlc-android/$version/'; 'https://plug-mirror.rcac.purdue.edu/vlc/vlc-android/$version/';
List<String> apkUrls = []; List<String> apkUrls = [];

View File

@ -14,21 +14,21 @@ class WhatsApp extends AppSource {
} }
@override @override
Future<String> apkUrlPrefetchModifier(String apkUrl) async { Future<String> apkUrlPrefetchModifier(
Response res = await get(Uri.parse('https://www.whatsapp.com/android')); String apkUrl, String standardUrl) async {
Response res = await sourceRequest('https://www.whatsapp.com/android');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var targetLinks = parse(res.body) var targetLinks = parse(res.body)
.querySelectorAll('a') .querySelectorAll('a')
.map((e) => e.attributes['href']) .map((e) => e.attributes['href'] ?? '')
.where((e) => e != null) .where((e) => e.isNotEmpty)
.where((e) => .where((e) =>
e!.contains('scontent.whatsapp.net') && e.contains('content.whatsapp.net') && e.contains('WhatsApp.apk'))
e.contains('WhatsApp.apk'))
.toList(); .toList();
if (targetLinks.isEmpty) { if (targetLinks.isEmpty) {
throw NoAPKError(); throw NoAPKError();
} }
return targetLinks[0]!; return targetLinks[0];
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }
@ -39,7 +39,7 @@ class WhatsApp extends AppSource {
String standardUrl, String standardUrl,
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
Response res = await get(Uri.parse('https://www.whatsapp.com/android')); Response res = await sourceRequest('https://www.whatsapp.com/android');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var targetElements = parse(res.body) var targetElements = parse(res.body)
.querySelectorAll('p') .querySelectorAll('p')

View File

@ -302,8 +302,10 @@ class _AddAppPageState extends State<AddAppPage> {
'overrideSource', 'overrideSource',
defaultValue: HTML().runtimeType.toString(), defaultValue: HTML().runtimeType.toString(),
[ [
...sourceProvider.sources.map( ...sourceProvider.sources
(s) => MapEntry(s.runtimeType.toString(), s.name)) .where((s) => s.overrideEligible)
.map((s) =>
MapEntry(s.runtimeType.toString(), s.name))
], ],
label: tr('overrideSource')) label: tr('overrideSource'))
] ]

View File

@ -125,10 +125,13 @@ class AppsProvider with ChangeNotifier {
} }
downloadFile(String url, String fileName, Function? onProgress, downloadFile(String url, String fileName, Function? onProgress,
{bool useExisting = true}) async { {bool useExisting = true, Map<String, String>? headers}) async {
var destDir = (await getExternalCacheDirectories())!.first.path; var destDir = (await getExternalCacheDirectories())!.first.path;
StreamedResponse response = var req = Request('GET', Uri.parse(url));
await Client().send(Request('GET', Uri.parse(url))); if (headers != null) {
req.headers.addAll(headers);
}
StreamedResponse response = await Client().send(req);
File downloadedFile = File('$destDir/$fileName'); File downloadedFile = File('$destDir/$fileName');
if (!(downloadedFile.existsSync() && useExisting)) { if (!(downloadedFile.existsSync() && useExisting)) {
File tempDownloadedFile = File('${downloadedFile.path}.part'); File tempDownloadedFile = File('${downloadedFile.path}.part');
@ -170,15 +173,16 @@ class AppsProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
try { try {
String downloadUrl = await SourceProvider() AppSource source = SourceProvider()
.getSource(app.url, overrideSource: app.overrideSource) .getSource(app.url, overrideSource: app.overrideSource);
.apkUrlPrefetchModifier(app.apkUrls[app.preferredApkIndex].value); String downloadUrl = await source.apkUrlPrefetchModifier(
app.apkUrls[app.preferredApkIndex].value, app.url);
var fileName = '${app.id}-${downloadUrl.hashCode}.apk'; var fileName = '${app.id}-${downloadUrl.hashCode}.apk';
var notif = DownloadNotification(app.finalName, 100); var notif = DownloadNotification(app.finalName, 100);
notificationsProvider?.cancel(notif.id); notificationsProvider?.cancel(notif.id);
int? prevProg; int? prevProg;
File downloadedFile = File downloadedFile = await downloadFile(downloadUrl, fileName,
await downloadFile(downloadUrl, fileName, (double? progress) { headers: source.requestHeaders, (double? progress) {
int? prog = progress?.ceil(); int? prog = progress?.ceil();
if (apps[app.id] != null) { if (apps[app.id] != null) {
apps[app.id]!.downloadProgress = progress; apps[app.id]!.downloadProgress = progress;

View File

@ -7,7 +7,9 @@ import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:obtainium/app_sources/apkcombo.dart';
import 'package:obtainium/app_sources/apkmirror.dart'; import 'package:obtainium/app_sources/apkmirror.dart';
import 'package:obtainium/app_sources/apkpure.dart';
import 'package:obtainium/app_sources/codeberg.dart'; import 'package:obtainium/app_sources/codeberg.dart';
import 'package:obtainium/app_sources/fdroid.dart'; import 'package:obtainium/app_sources/fdroid.dart';
import 'package:obtainium/app_sources/fdroidrepo.dart'; import 'package:obtainium/app_sources/fdroidrepo.dart';
@ -20,6 +22,7 @@ import 'package:obtainium/app_sources/mullvad.dart';
import 'package:obtainium/app_sources/neutroncode.dart'; import 'package:obtainium/app_sources/neutroncode.dart';
import 'package:obtainium/app_sources/signal.dart'; import 'package:obtainium/app_sources/signal.dart';
import 'package:obtainium/app_sources/sourceforge.dart'; import 'package:obtainium/app_sources/sourceforge.dart';
import 'package:obtainium/app_sources/sourcehut.dart';
import 'package:obtainium/app_sources/steammobile.dart'; import 'package:obtainium/app_sources/steammobile.dart';
import 'package:obtainium/app_sources/telegramapp.dart'; import 'package:obtainium/app_sources/telegramapp.dart';
import 'package:obtainium/app_sources/vlc.dart'; import 'package:obtainium/app_sources/vlc.dart';
@ -315,6 +318,7 @@ abstract class AppSource {
late String name; late String name;
bool enforceTrackOnly = false; bool enforceTrackOnly = false;
bool changeLogIfAnyIsMarkDown = true; bool changeLogIfAnyIsMarkDown = true;
bool overrideEligible = false;
AppSource() { AppSource() {
name = runtimeType.toString(); name = runtimeType.toString();
@ -344,6 +348,18 @@ abstract class AppSource {
return url; return url;
} }
Map<String, String>? get requestHeaders => null;
Future<Response> sourceRequest(String url) async {
if (requestHeaders != null) {
var req = Request('GET', Uri.parse(url));
req.headers.addAll(requestHeaders!);
return Response.fromStream(await Client().send(req));
} else {
return get(Uri.parse(url));
}
}
String sourceSpecificStandardizeURL(String url) { String sourceSpecificStandardizeURL(String url) {
throw NotImplementedError(); throw NotImplementedError();
} }
@ -410,7 +426,8 @@ abstract class AppSource {
return null; return null;
} }
Future<String> apkUrlPrefetchModifier(String apkUrl) async { Future<String> apkUrlPrefetchModifier(
String apkUrl, String standardUrl) async {
return apkUrl; return apkUrl;
} }
@ -459,7 +476,10 @@ class SourceProvider {
FDroidRepo(), FDroidRepo(),
Jenkins(), Jenkins(),
SourceForge(), SourceForge(),
SourceHut(),
APKMirror(), APKMirror(),
APKPure(),
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
Mullvad(), Mullvad(),
Signal(), Signal(),
VLC(), VLC(),