diff --git a/lib/app_sources/apkpure.dart b/lib/app_sources/apkpure.dart index 37742a2..db6c929 100644 --- a/lib/app_sources/apkpure.dart +++ b/lib/app_sources/apkpure.dart @@ -1,24 +1,18 @@ +import 'dart:convert'; + import 'package:device_info_plus/device_info_plus.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/custom_errors.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 +extension Unique on List { + List unique([Id Function(E element)? id, bool inplace = true]) { + final ids = Set(); + var list = inplace ? this : List.from(this); + list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id)); + return list; } - return releaseDate; } class APKPure extends AppSource { @@ -35,6 +29,10 @@ class APKPure extends AppSource { [ GeneratedFormSwitch('stayOneVersionBehind', 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; } - getDetailsForVersionLink( - String standardUrl, - String appId, - String host, - List supportedArchs, - String link, + getDetailsForVersion(List versionVariants, List supportedArchs, Map additionalSettings) async { - var res = await sourceRequest(link, additionalSettings); - 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) { - String architectureString = e.text.trim(); - if (architectureString.toLowerCase() == 'unlimited' || - architectureString.toLowerCase() == 'universal') { - architectureString = ''; - } - List 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 && - architectures.isNotEmpty && - architectures - .where((a) => supportedArchs.contains(a)) - .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 - return MapEntry( - '$appId-$versionCode-$architectureString.${type.toLowerCase()}', - 'https://d.${hosts.contains(host) ? 'cdnpure.com' : host}/b/$type/$appId?versionCode=$versionCode'); - }) - .where((e) => e.key.isNotEmpty) - .toList() ?? - []; - if (apkUrls.isEmpty) { - var link = - html.querySelector("a.download-start-btn")?.attributes['href']; - RegExp downloadLinkRegEx = RegExp( - r'^https:\/\/d\.[^/]+\/b\/([^/]+)\/[^/?]+\?versionCode=([0-9]+)$', - caseSensitive: false); - RegExpMatch? match = downloadLinkRegEx.firstMatch(link ?? ''); - if (match == null) { - throw NoAPKError(); - } - String type = match.group(1)!; - String versionCode = match.group(2)!; - apkUrls = [ - 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("
", " \n"); - return APKDetails(version, apkUrls, AppNames(author, appName), - releaseDate: topReleaseDate, changeLog: changeLog); + var apkUrls = versionVariants + .map((e) { + String appId = e['package_name']; + String versionCode = e['version_code']; + + List architectures = e['native_code']?.cast(); + String architectureString = architectures.join(','); + if (additionalSettings['autoApkFilterByArch'] == true && + architectures.isNotEmpty && + architectures.where((a) => supportedArchs.contains(a)).isEmpty) { + return null; + } + + String type = e['asset']['type']; + if (e['is_a_p_ks'] == true) { + type = 'APK'; + } + + return MapEntry( + '$appId-$versionCode-$architectureString.${type.toLowerCase()}', + 'https://d.cdnpure.com/b/$type/$appId?versionCode=$versionCode&nc=$architectureString'); + }) + .nonNulls + .toList() + .unique((e) => e.key); + + // get version details from first variant + var v = versionVariants.first; + String version = v['version_name']; + String author = v['developer']; + String appName = v['title']; + DateTime releaseDate = DateTime.parse(v['update_date']); + String? changeLog = v['whatsnew']; + if (changeLog != null && changeLog.isEmpty) { + changeLog = null; + } + + if (additionalSettings['selectNewestApk'] == true) { + apkUrls = [apkUrls.first]; + } + + return APKDetails(version, apkUrls, AppNames(author, appName), + releaseDate: releaseDate, changeLog: changeLog); + } + + @override + Future?> getRequestHeaders( + Map additionalSettings, + {bool forAPKDownload = false}) async { + if (forAPKDownload) { + return null; } 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 additionalSettings, ) async { String appId = (await tryInferringAppId(standardUrl))!; - String host = Uri.parse(standardUrl).host; - var res0 = await sourceRequest('$standardUrl/versions', additionalSettings); - var decodedStandardUrl = standardUrl; - try { - decodedStandardUrl = Uri.decodeFull(decodedStandardUrl); - } catch (e) { - // + List supportedArchs = + (await DeviceInfoPlugin().androidInfo).supportedAbis; + + // request versions from API + 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, { - 'skipSort': true, - 'customLinkFilterRegex': '$decodedStandardUrl/download/[^/]+\$' - }); + List> apks = + jsonDecode(res.body)['version_list'].cast>(); - var supportedArchs = (await DeviceInfoPlugin().androidInfo).supportedAbis; + // group by version + List versions = apks + .fold>>>({}, + (Map>> val, + Map element) { + String v = element['version_name']; + if (!val.containsKey(v)) { + val[v] = []; + } + val[v]?.add(element); + return val; + }) + .values + .toList(); - if (additionalSettings['autoApkFilterByArch'] != true) { - // No need to request multiple versions when we're not going to filter them (always pick the top one) - versionLinks = versionLinks.sublist(0, 1); - } - if (versionLinks.isEmpty) { - throw NoReleasesError(); - } - - for (var i = 0; i < versionLinks.length; i++) { - var link = versionLinks[i]; + for (var i = 0; i < versions.length; i++) { + var v = versions[i]; try { if (i == 0 && additionalSettings['stayOneVersionBehind'] == true) { throw NoReleasesError(); } - return await getDetailsForVersionLink(standardUrl, appId, host, - supportedArchs, link.key, additionalSettings); + return await getDetailsForVersion( + v, supportedArchs, additionalSettings); } catch (e) { if (additionalSettings['fallbackToOlderReleases'] != true || - i == versionLinks.length - 1) { + i == versions.length - 1) { rethrow; } } diff --git a/lib/app_sources/uptodown.dart b/lib/app_sources/uptodown.dart index fa447b8..134b64c 100644 --- a/lib/app_sources/uptodown.dart +++ b/lib/app_sources/uptodown.dart @@ -1,9 +1,23 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:html/parser.dart'; -import 'package:obtainium/app_sources/apkpure.dart'; import 'package:obtainium/custom_errors.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 { Uptodown() { hosts = ['uptodown.com'];