Add Aptoide as a Source (#843)

This commit is contained in:
Imran Remtulla
2023-09-02 21:28:30 -04:00
parent e215585a64
commit 75a8dc9ee4
3 changed files with 112 additions and 2 deletions

View File

@ -16,6 +16,7 @@ Currently supported App sources:
- [Signal](https://signal.org/) - [Signal](https://signal.org/)
- [SourceForge](https://sourceforge.net/) - [SourceForge](https://sourceforge.net/)
- [SourceHut](https://git.sr.ht/) - [SourceHut](https://git.sr.ht/)
- [Aptoide](https://aptoide.com/)
- [APKMirror](https://apkmirror.com/) (Track-Only) - [APKMirror](https://apkmirror.com/) (Track-Only)
- [APKPure](https://apkpure.com/) - [APKPure](https://apkpure.com/)
- [Huawei AppGallery](https://appgallery.huawei.com/) - [Huawei AppGallery](https://appgallery.huawei.com/)

View File

@ -0,0 +1,104 @@
import 'dart:convert';
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 Aptoide extends AppSource {
Aptoide() {
host = 'aptoide.com';
name = tr('Aptoide');
allowSubDomains = true;
}
@override
String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) {
throw InvalidURLError(name);
}
return url.substring(0, match.end);
}
@override
Future<String?> tryInferringAppId(String standardUrl,
{Map<String, dynamic> additionalSettings = const {}}) async {
return (await getLatestAPKDetails(standardUrl, additionalSettings)).version;
}
@override
Future<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> additionalSettings,
) async {
var res = await sourceRequest(standardUrl);
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
}
var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.body);
String? id;
if (idMatch != null) {
id = res.body.substring(idMatch.start + 12, idMatch.end);
} else {
throw NoReleasesError();
}
var res2 =
await sourceRequest('https://ws2.aptoide.com/api/7/getApp/app_id/$id');
if (res2.statusCode != 200) {
throw getObtainiumHttpError(res);
}
var appDetails = jsonDecode(res2.body)?['nodes']?['meta']?['data'];
String appName = appDetails?['name'] ?? tr('app');
String author = appDetails?['developer']?['name'] ?? name;
String? dateStr = appDetails?['updated'];
String? version = appDetails?['file']?['vername'];
String? apkUrl = appDetails?['file']?['path'];
if (version == null) {
throw NoVersionError();
}
if (apkUrl == null) {
throw NoAPKError();
}
DateTime? relDate;
if (dateStr != null) {
relDate = DateTime.parse(dateStr);
}
return APKDetails(
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
releaseDate: relDate);
}
@override
Future<Map<String, List<String>>> search(String query,
{Map<String, dynamic> querySettings = const {}}) async {
Response res = await sourceRequest(
'https://search.$host/?q=${Uri.encodeQueryComponent(query)}');
if (res.statusCode == 200) {
Map<String, List<String>> urlsWithDescriptions = {};
parse(res.body).querySelectorAll('.package-header').forEach((e) {
String? url = e.attributes['href'];
if (url != null) {
try {
standardizeUrl(url);
} catch (e) {
url = null;
}
}
if (url != null) {
urlsWithDescriptions[url] = [
e.querySelector('.package-name')?.text.trim() ?? '',
e.querySelector('.package-summary')?.text.trim() ??
tr('noDescription')
];
}
});
return urlsWithDescriptions;
} else {
throw getObtainiumHttpError(res);
}
}
}

View File

@ -9,6 +9,7 @@ import 'package:html/dom.dart';
import 'package:http/http.dart'; import 'package:http/http.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/apkpure.dart';
import 'package:obtainium/app_sources/aptoide.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';
@ -325,6 +326,7 @@ abstract class AppSource {
bool enforceTrackOnly = false; bool enforceTrackOnly = false;
bool changeLogIfAnyIsMarkDown = true; bool changeLogIfAnyIsMarkDown = true;
bool appIdInferIsOptional = false; bool appIdInferIsOptional = false;
bool allowSubDomains = false;
AppSource() { AppSource() {
name = runtimeType.toString(); name = runtimeType.toString();
@ -522,13 +524,14 @@ class SourceProvider {
Jenkins(), Jenkins(),
SourceForge(), SourceForge(),
SourceHut(), SourceHut(),
Aptoide(),
APKMirror(), APKMirror(),
APKPure(), APKPure(),
HuaweiAppGallery(), HuaweiAppGallery(),
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden) // APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
Mullvad(), Mullvad(),
Signal(), Signal(),
VLC(), // As of 2023-08-26 this site randomly messes up the 'latest' version (one minute it's 3.5.4, next minute back to 3.5.3) VLC(),
// WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date // WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date
TelegramApp(), TelegramApp(),
SteamMobile(), SteamMobile(),
@ -554,7 +557,9 @@ class SourceProvider {
} }
AppSource? source; AppSource? source;
for (var s in sources.where((element) => element.host != null)) { for (var s in sources.where((element) => element.host != null)) {
if (RegExp('://${s.host}(/|\\z)?').hasMatch(url)) { if (RegExp(
'://${s.allowSubDomains ? '([^\\.]+\\.)*' : ''}${s.host}(/|\\z)?')
.hasMatch(url)) {
source = s; source = s;
break; break;
} }