mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-10-27 03:33:45 +01: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();
 | |
|   }
 | |
| }
 |