mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-30 12:30:16 +02:00
208 lines
7.5 KiB
Dart
208 lines
7.5 KiB
Dart
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
|
|
}
|
|
return releaseDate;
|
|
}
|
|
|
|
class APKPure extends AppSource {
|
|
APKPure() {
|
|
hosts = ['apkpure.net', 'apkpure.com'];
|
|
allowSubDomains = true;
|
|
naiveStandardVersionDetection = true;
|
|
showReleaseDateAsVersionToggle = true;
|
|
additionalSourceAppSpecificSettingFormItems = [
|
|
[
|
|
GeneratedFormSwitch('fallbackToOlderReleases',
|
|
label: tr('fallbackToOlderReleases'), defaultValue: true)
|
|
],
|
|
[
|
|
GeneratedFormSwitch('stayOneVersionBehind',
|
|
label: tr('stayOneVersionBehind'), defaultValue: false)
|
|
]
|
|
];
|
|
}
|
|
|
|
@override
|
|
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
|
|
RegExp standardUrlRegExB = RegExp(
|
|
'^https?://m.${getSourceRegex(hosts)}(/+[^/]{2})?/+[^/]+/+[^/]+',
|
|
caseSensitive: false);
|
|
RegExpMatch? match = standardUrlRegExB.firstMatch(url);
|
|
if (match != null) {
|
|
var uri = Uri.parse(url);
|
|
url = 'https://${uri.host.substring(2)}${uri.path}';
|
|
}
|
|
RegExp standardUrlRegExA = RegExp(
|
|
'^https?://(www\\.)?${getSourceRegex(hosts)}(/+[^/]{2})?/+[^/]+/+[^/]+',
|
|
caseSensitive: false);
|
|
match = standardUrlRegExA.firstMatch(url);
|
|
if (match == null) {
|
|
throw InvalidURLError(name);
|
|
}
|
|
return match.group(0)!;
|
|
}
|
|
|
|
@override
|
|
Future<String?> tryInferringAppId(String standardUrl,
|
|
{Map<String, dynamic> additionalSettings = const {}}) async {
|
|
return Uri.parse(standardUrl).pathSegments.last;
|
|
}
|
|
|
|
getDetailsForVersionLink(
|
|
String standardUrl,
|
|
String appId,
|
|
String host,
|
|
List<String> supportedArchs,
|
|
String link,
|
|
Map<String, dynamic> 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<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 &&
|
|
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) {
|
|
throw NoAPKError();
|
|
}
|
|
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),
|
|
releaseDate: topReleaseDate, changeLog: changeLog);
|
|
} else {
|
|
throw getObtainiumHttpError(res);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<APKDetails> getLatestAPKDetails(
|
|
String standardUrl,
|
|
Map<String, dynamic> 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) {
|
|
//
|
|
}
|
|
var versionLinks = await grabLinksCommon(res0, {
|
|
'skipSort': true,
|
|
'customLinkFilterRegex': '$decodedStandardUrl/download/[^/]+\$'
|
|
});
|
|
|
|
var supportedArchs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
|
|
|
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];
|
|
try {
|
|
if (i == 0 && additionalSettings['stayOneVersionBehind'] == true) {
|
|
throw NoReleasesError();
|
|
}
|
|
return await getDetailsForVersionLink(standardUrl, appId, host,
|
|
supportedArchs, link.key, additionalSettings);
|
|
} catch (e) {
|
|
if (additionalSettings['fallbackToOlderReleases'] != true ||
|
|
i == versionLinks.length - 1) {
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|
|
throw NoAPKError();
|
|
}
|
|
}
|