rewrite apkpure source to use api instead of web scraping

This commit is contained in:
bernikr
2025-05-19 18:17:32 +02:00
parent 7d01141db5
commit 3345b26fa9
2 changed files with 114 additions and 141 deletions

View File

@@ -1,24 +1,18 @@
import 'dart:convert';
import 'package:device_info_plus/device_info_plus.dart'; 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/parser.dart';
import 'package:obtainium/app_sources/html.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.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';
parseDateTimeMMMddCommayyyy(String? dateString) { extension Unique<E, Id> on List<E> {
DateTime? releaseDate; List<E> unique([Id Function(E element)? id, bool inplace = true]) {
try { final ids = Set();
releaseDate = dateString != null var list = inplace ? this : List<E>.from(this);
? DateFormat('MMM dd, yyyy').parse(dateString) list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id));
: null; return list;
releaseDate = dateString != null && releaseDate == null
? DateFormat('MMMM dd, yyyy').parse(dateString)
: releaseDate;
} catch (err) {
// ignore
} }
return releaseDate;
} }
class APKPure extends AppSource { class APKPure extends AppSource {
@@ -35,6 +29,10 @@ class APKPure extends AppSource {
[ [
GeneratedFormSwitch('stayOneVersionBehind', GeneratedFormSwitch('stayOneVersionBehind',
label: tr('stayOneVersionBehind'), defaultValue: false) label: tr('stayOneVersionBehind'), defaultValue: false)
],
[
GeneratedFormSwitch('selectNewestApk',
label: tr('selectNewestApk'), defaultValue: true)
] ]
]; ];
} }
@@ -65,109 +63,65 @@ class APKPure extends AppSource {
return Uri.parse(standardUrl).pathSegments.last; return Uri.parse(standardUrl).pathSegments.last;
} }
getDetailsForVersionLink( getDetailsForVersion(List<Map> versionVariants, List<String> supportedArchs,
String standardUrl,
String appId,
String host,
List<String> supportedArchs,
String link,
Map<String, dynamic> additionalSettings) async { Map<String, dynamic> additionalSettings) async {
var res = await sourceRequest(link, additionalSettings); var apkUrls = versionVariants
if (res.statusCode == 200) {
var html = parse(res.body);
var apksDiv =
html.querySelector('#version-list div div.show-more-content');
DateTime? topReleaseDate;
var apkUrls = apksDiv
?.querySelectorAll('div.group-title')
.map((e) { .map((e) {
String architectureString = e.text.trim(); String appId = e['package_name'];
if (architectureString.toLowerCase() == 'unlimited' || String versionCode = e['version_code'];
architectureString.toLowerCase() == 'universal') {
architectureString = ''; List<String> architectures = e['native_code']?.cast<String>();
} String architectureString = architectures.join(',');
List<String> architectures = architectureString
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
// Only take the first APK for each architecture, ignore others for now, for simplicity
// Unclear why there can even be multiple APKs for the same version and arch
var apkInfo = e.nextElementSibling?.querySelector('div.info');
String? versionCode = RegExp('[0-9]+')
.firstMatch(
apkInfo?.querySelector('div.info-top .code')?.text ??
'')
?.group(0)
?.trim();
var types = apkInfo
?.querySelectorAll('div.info-top span.tag')
.map((e) => e.text.trim())
.map((t) => t == 'APKs' ? 'APK' : t) ??
[];
String type = types.isEmpty ? 'APK' : types.first;
String? dateString = apkInfo
?.querySelector('div.info-bottom span.time')
?.text
.trim();
DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString);
if (additionalSettings['autoApkFilterByArch'] == true && if (additionalSettings['autoApkFilterByArch'] == true &&
architectures.isNotEmpty && architectures.isNotEmpty &&
architectures architectures.where((a) => supportedArchs.contains(a)).isEmpty) {
.where((a) => supportedArchs.contains(a)) return null;
.isEmpty) {
return const MapEntry('', '');
} }
topReleaseDate ??=
releaseDate; // Just use the release date of the first APK in the list as the release date for this version String type = e['asset']['type'];
if (e['is_a_p_ks'] == true) {
type = 'APK';
}
return MapEntry( return MapEntry(
'$appId-$versionCode-$architectureString.${type.toLowerCase()}', '$appId-$versionCode-$architectureString.${type.toLowerCase()}',
'https://d.${hosts.contains(host) ? 'cdnpure.com' : host}/b/$type/$appId?versionCode=$versionCode'); 'https://d.cdnpure.com/b/$type/$appId?versionCode=$versionCode&nc=$architectureString');
}) })
.where((e) => e.key.isNotEmpty) .nonNulls
.toList() ?? .toList()
[]; .unique((e) => e.key);
if (apkUrls.isEmpty) {
var link = // get version details from first variant
html.querySelector("a.download-start-btn")?.attributes['href']; var v = versionVariants.first;
RegExp downloadLinkRegEx = RegExp( String version = v['version_name'];
r'^https:\/\/d\.[^/]+\/b\/([^/]+)\/[^/?]+\?versionCode=([0-9]+)$', String author = v['developer'];
caseSensitive: false); String appName = v['title'];
RegExpMatch? match = downloadLinkRegEx.firstMatch(link ?? ''); DateTime releaseDate = DateTime.parse(v['update_date']);
if (match == null) { String? changeLog = v['whatsnew'];
throw NoAPKError(); if (changeLog != null && changeLog.isEmpty) {
changeLog = null;
} }
String type = match.group(1)!;
String versionCode = match.group(2)!; if (additionalSettings['selectNewestApk'] == true) {
apkUrls = [ apkUrls = [apkUrls.first];
MapEntry('$appId-$versionCode-.${type.toLowerCase()}',
'https://d.${hosts.contains(host) ? 'cdnpure.com' : host}/b/$type/$appId?versionCode=$versionCode')
];
} }
String version = Uri.parse(link).pathSegments.last;
String? author;
try {
author = html
.querySelector('span.info-sdk')
?.text
.trim()
.substring(version.length + 4) ??
Uri.parse(standardUrl).pathSegments.reversed.last;
} catch (e) {
author = html.querySelector('span.info-sdk')?.text.trim() ??
Uri.parse(standardUrl).pathSegments.reversed.last;
}
String appName =
html.querySelector('h1.info-title')?.text.trim() ?? appId;
String? changeLog = html
.querySelector('div.module.change-log')
?.innerHtml
.trim()
.replaceAll("<br>", " \n");
return APKDetails(version, apkUrls, AppNames(author, appName), return APKDetails(version, apkUrls, AppNames(author, appName),
releaseDate: topReleaseDate, changeLog: changeLog); releaseDate: releaseDate, changeLog: changeLog);
}
@override
Future<Map<String, String>?> getRequestHeaders(
Map<String, dynamic> additionalSettings,
{bool forAPKDownload = false}) async {
if (forAPKDownload) {
return null;
} else { } else {
throw getObtainiumHttpError(res); return {
"Ual-Access-Businessid": "projecta",
"Ual-Access-ProjectA":
'{"device_info":{"os_ver":"${((await DeviceInfoPlugin().androidInfo).version.sdkInt)}"}}',
};
} }
} }
@@ -177,41 +131,46 @@ class APKPure extends AppSource {
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
String appId = (await tryInferringAppId(standardUrl))!; String appId = (await tryInferringAppId(standardUrl))!;
String host = Uri.parse(standardUrl).host;
var res0 = await sourceRequest('$standardUrl/versions', additionalSettings); List<String> supportedArchs =
var decodedStandardUrl = standardUrl; (await DeviceInfoPlugin().androidInfo).supportedAbis;
try {
decodedStandardUrl = Uri.decodeFull(decodedStandardUrl); // request versions from API
} catch (e) { var res = await sourceRequest(
// "https://tapi.pureapk.com/v3/get_app_his_version?package_name=$appId&hl=en",
additionalSettings);
if (res.statusCode != 200) {
throw getObtainiumHttpError(res);
} }
var versionLinks = await grabLinksCommon(res0, { List<Map<String, dynamic>> apks =
'skipSort': true, jsonDecode(res.body)['version_list'].cast<Map<String, dynamic>>();
'customLinkFilterRegex': '$decodedStandardUrl/download/[^/]+\$'
});
var supportedArchs = (await DeviceInfoPlugin().androidInfo).supportedAbis; // group by version
List versions = apks
if (additionalSettings['autoApkFilterByArch'] != true) { .fold<Map<String, List<Map<String, dynamic>>>>({},
// No need to request multiple versions when we're not going to filter them (always pick the top one) (Map<String, List<Map<String, dynamic>>> val,
versionLinks = versionLinks.sublist(0, 1); Map<String, dynamic> element) {
} String v = element['version_name'];
if (versionLinks.isEmpty) { if (!val.containsKey(v)) {
throw NoReleasesError(); val[v] = [];
} }
val[v]?.add(element);
return val;
})
.values
.toList();
for (var i = 0; i < versionLinks.length; i++) { for (var i = 0; i < versions.length; i++) {
var link = versionLinks[i]; var v = versions[i];
try { try {
if (i == 0 && additionalSettings['stayOneVersionBehind'] == true) { if (i == 0 && additionalSettings['stayOneVersionBehind'] == true) {
throw NoReleasesError(); throw NoReleasesError();
} }
return await getDetailsForVersionLink(standardUrl, appId, host, return await getDetailsForVersion(
supportedArchs, link.key, additionalSettings); v, supportedArchs, additionalSettings);
} catch (e) { } catch (e) {
if (additionalSettings['fallbackToOlderReleases'] != true || if (additionalSettings['fallbackToOlderReleases'] != true ||
i == versionLinks.length - 1) { i == versions.length - 1) {
rethrow; rethrow;
} }
} }

View File

@@ -1,9 +1,23 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:obtainium/app_sources/apkpure.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';
parseDateTimeMMMddCommayyyy(String? dateString) {
DateTime? releaseDate;
try {
releaseDate = dateString != null
? DateFormat('MMM dd, yyyy').parse(dateString)
: null;
releaseDate = dateString != null && releaseDate == null
? DateFormat('MMMM dd, yyyy').parse(dateString)
: releaseDate;
} catch (err) {
// ignore
}
return releaseDate;
}
class Uptodown extends AppSource { class Uptodown extends AppSource {
Uptodown() { Uptodown() {
hosts = ['uptodown.com']; hosts = ['uptodown.com'];