mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-04 07:13:28 +01:00 
			
		
		
		
	Added APKMirror (Phew!)
This commit is contained in:
		@@ -13,6 +13,7 @@ Currently supported App sources:
 | 
				
			|||||||
- [IzzyOnDroid](https://android.izzysoft.de/)
 | 
					- [IzzyOnDroid](https://android.izzysoft.de/)
 | 
				
			||||||
- [Mullvad](https://mullvad.net/en/)
 | 
					- [Mullvad](https://mullvad.net/en/)
 | 
				
			||||||
- [Signal](https://signal.org/)
 | 
					- [Signal](https://signal.org/)
 | 
				
			||||||
 | 
					- [APKMirror](https://apkmirror.com/)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Limitations
 | 
					## Limitations
 | 
				
			||||||
- App installs are assumed to have succeeded; failures and cancelled installs cannot be detected.
 | 
					- App installs are assumed to have succeeded; failures and cancelled installs cannot be detected.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										112
									
								
								lib/app_sources/apkmirror.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								lib/app_sources/apkmirror.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,112 @@
 | 
				
			|||||||
 | 
					import 'package:html/parser.dart';
 | 
				
			||||||
 | 
					import 'package:http/http.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/components/generated_form.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/providers/source_provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class APKMirror implements AppSource {
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  late String host = 'apkmirror.com';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String standardizeURL(String url) {
 | 
				
			||||||
 | 
					    RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+');
 | 
				
			||||||
 | 
					    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
				
			||||||
 | 
					    if (match == null) {
 | 
				
			||||||
 | 
					      throw notValidURL(runtimeType.toString());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return url.substring(0, match.end);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) =>
 | 
				
			||||||
 | 
					      '$standardUrl#whatsnew';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async {
 | 
				
			||||||
 | 
					    var originalUri = Uri.parse(apkUrl);
 | 
				
			||||||
 | 
					    var res = await get(originalUri);
 | 
				
			||||||
 | 
					    if (res.statusCode != 200) {
 | 
				
			||||||
 | 
					      throw false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var href =
 | 
				
			||||||
 | 
					        parse(res.body).querySelector('.downloadButton')?.attributes['href'];
 | 
				
			||||||
 | 
					    if (href == null) {
 | 
				
			||||||
 | 
					      throw false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var res2 = await get(Uri.parse('${originalUri.origin}$href'), headers: {
 | 
				
			||||||
 | 
					      'User-Agent':
 | 
				
			||||||
 | 
					          'Mozilla/5.0 (X11; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0'
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (res2.statusCode != 200) {
 | 
				
			||||||
 | 
					      throw false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var links = parse(res2.body)
 | 
				
			||||||
 | 
					        .querySelectorAll('a')
 | 
				
			||||||
 | 
					        .where((element) => element.innerHtml == 'here')
 | 
				
			||||||
 | 
					        .map((e) => e.attributes['href'])
 | 
				
			||||||
 | 
					        .where((element) => element != null)
 | 
				
			||||||
 | 
					        .toList();
 | 
				
			||||||
 | 
					    if (links.isEmpty) {
 | 
				
			||||||
 | 
					      throw false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return '${originalUri.origin}${links[0]}';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 | 
					    Response res = await get(Uri.parse('$standardUrl/feed'));
 | 
				
			||||||
 | 
					    if (res.statusCode != 200) {
 | 
				
			||||||
 | 
					      throw couldNotFindReleases;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var nextUrl = parse(res.body)
 | 
				
			||||||
 | 
					        .querySelector('item')
 | 
				
			||||||
 | 
					        ?.querySelector('link')
 | 
				
			||||||
 | 
					        ?.nextElementSibling
 | 
				
			||||||
 | 
					        ?.innerHtml;
 | 
				
			||||||
 | 
					    if (nextUrl == null) {
 | 
				
			||||||
 | 
					      throw couldNotFindReleases;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    Response res2 = await get(Uri.parse(nextUrl), headers: {
 | 
				
			||||||
 | 
					      'User-Agent':
 | 
				
			||||||
 | 
					          'Mozilla/5.0 (X11; Linux x86_64; rv:105.0) Gecko/20100101 Firefox/105.0'
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (res2.statusCode != 200) {
 | 
				
			||||||
 | 
					      throw couldNotFindReleases;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var html2 = parse(res2.body);
 | 
				
			||||||
 | 
					    var origin = Uri.parse(standardUrl).origin;
 | 
				
			||||||
 | 
					    List<String> apkUrls = html2
 | 
				
			||||||
 | 
					        .querySelectorAll('.apkm-badge')
 | 
				
			||||||
 | 
					        .map((e) => e.innerHtml != 'APK'
 | 
				
			||||||
 | 
					            ? ''
 | 
				
			||||||
 | 
					            : e.previousElementSibling?.attributes['href'] ?? '')
 | 
				
			||||||
 | 
					        .where((element) => element.isNotEmpty)
 | 
				
			||||||
 | 
					        .map((e) => '$origin$e')
 | 
				
			||||||
 | 
					        .toList();
 | 
				
			||||||
 | 
					    if (apkUrls.isEmpty) {
 | 
				
			||||||
 | 
					      throw noAPKFound;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var version = html2.querySelector('span.active.accent_color')?.innerHtml;
 | 
				
			||||||
 | 
					    if (version == null) {
 | 
				
			||||||
 | 
					      throw couldNotFindLatestVersion;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return APKDetails(version, apkUrls);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  AppNames getAppNames(String standardUrl) {
 | 
				
			||||||
 | 
					    String temp = standardUrl.substring(standardUrl.indexOf('://') + 3);
 | 
				
			||||||
 | 
					    List<String> names = temp.substring(temp.indexOf('/') + 1).split('/');
 | 
				
			||||||
 | 
					    return AppNames(names[1], names[2]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  List<List<GeneratedFormItem>> additionalDataFormItems = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  List<String> additionalDataDefaults = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  List<GeneratedFormItem> moreSourceSettingsFormItems = [];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -26,6 +26,9 @@ class FDroid implements AppSource {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,6 +33,9 @@ class GitHub implements AppSource {
 | 
				
			|||||||
  String? changeLogPageFromStandardUrl(String standardUrl) =>
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) =>
 | 
				
			||||||
      '$standardUrl/releases';
 | 
					      '$standardUrl/releases';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,6 +22,9 @@ class GitLab implements AppSource {
 | 
				
			|||||||
  String? changeLogPageFromStandardUrl(String standardUrl) =>
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) =>
 | 
				
			||||||
      '$standardUrl/-/releases';
 | 
					      '$standardUrl/-/releases';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,9 @@ class IzzyOnDroid implements AppSource {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,9 @@ class Mullvad implements AppSource {
 | 
				
			|||||||
  String? changeLogPageFromStandardUrl(String standardUrl) =>
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) =>
 | 
				
			||||||
      'https://github.com/mullvad/mullvadvpn-app/blob/master/CHANGELOG.md';
 | 
					      'https://github.com/mullvad/mullvadvpn-app/blob/master/CHANGELOG.md';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,9 @@ class Signal implements AppSource {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,9 @@ class SourceForge implements AppSource {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl) => null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl) async => apkUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  Future<APKDetails> getLatestAPKDetails(
 | 
				
			||||||
      String standardUrl, List<String> additionalData) async {
 | 
					      String standardUrl, List<String> additionalData) async {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,6 +64,9 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<ApkFile> downloadApp(String apkUrl, String appId) async {
 | 
					  Future<ApkFile> downloadApp(String apkUrl, String appId) async {
 | 
				
			||||||
 | 
					    apkUrl = await SourceProvider()
 | 
				
			||||||
 | 
					        .getSource(apps[appId]!.app.url)
 | 
				
			||||||
 | 
					        .apkUrlPrefetchModifier(apkUrl);
 | 
				
			||||||
    StreamedResponse response =
 | 
					    StreamedResponse response =
 | 
				
			||||||
        await Client().send(Request('GET', Uri.parse(apkUrl)));
 | 
					        await Client().send(Request('GET', Uri.parse(apkUrl)));
 | 
				
			||||||
    File downloadFile =
 | 
					    File downloadFile =
 | 
				
			||||||
@@ -420,7 +423,10 @@ class _APKPickerState extends State<APKPicker> {
 | 
				
			|||||||
        Text('${widget.app.name} has more than one package:'),
 | 
					        Text('${widget.app.name} has more than one package:'),
 | 
				
			||||||
        const SizedBox(height: 16),
 | 
					        const SizedBox(height: 16),
 | 
				
			||||||
        ...widget.app.apkUrls.map((u) => RadioListTile<String>(
 | 
					        ...widget.app.apkUrls.map((u) => RadioListTile<String>(
 | 
				
			||||||
            title: Text(Uri.parse(u).pathSegments.last),
 | 
					            title: Text(Uri.parse(u)
 | 
				
			||||||
 | 
					                .pathSegments
 | 
				
			||||||
 | 
					                .where((element) => element.isNotEmpty)
 | 
				
			||||||
 | 
					                .last),
 | 
				
			||||||
            value: u,
 | 
					            value: u,
 | 
				
			||||||
            groupValue: apkUrl,
 | 
					            groupValue: apkUrl,
 | 
				
			||||||
            onChanged: (String? val) {
 | 
					            onChanged: (String? val) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@
 | 
				
			|||||||
import 'dart:convert';
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:html/dom.dart';
 | 
					import 'package:html/dom.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/app_sources/apkmirror.dart';
 | 
				
			||||||
import 'package:obtainium/app_sources/fdroid.dart';
 | 
					import 'package:obtainium/app_sources/fdroid.dart';
 | 
				
			||||||
import 'package:obtainium/app_sources/github.dart';
 | 
					import 'package:obtainium/app_sources/github.dart';
 | 
				
			||||||
import 'package:obtainium/app_sources/gitlab.dart';
 | 
					import 'package:obtainium/app_sources/gitlab.dart';
 | 
				
			||||||
@@ -137,6 +138,7 @@ abstract class AppSource {
 | 
				
			|||||||
  late List<String> additionalDataDefaults;
 | 
					  late List<String> additionalDataDefaults;
 | 
				
			||||||
  late List<GeneratedFormItem> moreSourceSettingsFormItems;
 | 
					  late List<GeneratedFormItem> moreSourceSettingsFormItems;
 | 
				
			||||||
  String? changeLogPageFromStandardUrl(String standardUrl);
 | 
					  String? changeLogPageFromStandardUrl(String standardUrl);
 | 
				
			||||||
 | 
					  Future<String> apkUrlPrefetchModifier(String apkUrl);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
abstract class MassAppSource {
 | 
					abstract class MassAppSource {
 | 
				
			||||||
@@ -154,7 +156,8 @@ class SourceProvider {
 | 
				
			|||||||
    IzzyOnDroid(),
 | 
					    IzzyOnDroid(),
 | 
				
			||||||
    Mullvad(),
 | 
					    Mullvad(),
 | 
				
			||||||
    Signal(),
 | 
					    Signal(),
 | 
				
			||||||
    SourceForge()
 | 
					    SourceForge(),
 | 
				
			||||||
 | 
					    APKMirror()
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Add more mass source classes here so they are available via the service
 | 
					  // Add more mass source classes here so they are available via the service
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user