mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-08-17 04:08:09 +02:00
Add Uptodown (#853)
This commit is contained in:
47
README.md
47
README.md
@@ -2,31 +2,36 @@
|
|||||||
|
|
||||||
Get Android App Updates Directly From the Source.
|
Get Android App Updates Directly From the Source.
|
||||||
|
|
||||||
Obtainium allows you to install and update Open-Source Apps directly from their releases pages, and receive notifications when new releases are made available.
|
Obtainium allows you to install and update Apps directly from their releases pages, and receive notifications when new releases are made available.
|
||||||
|
|
||||||
Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0)
|
Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0)
|
||||||
|
|
||||||
Currently supported App sources:
|
Currently supported App sources:
|
||||||
- [GitHub](https://github.com/)
|
- Open Source - General:
|
||||||
- [GitLab](https://gitlab.com/)
|
- [GitHub](https://github.com/)
|
||||||
- [Codeberg](https://codeberg.org/)
|
- [GitLab](https://gitlab.com/)
|
||||||
- [F-Droid](https://f-droid.org/)
|
- [Codeberg](https://codeberg.org/)
|
||||||
- [IzzyOnDroid](https://android.izzysoft.de/)
|
- [F-Droid](https://f-droid.org/)
|
||||||
- [Mullvad](https://mullvad.net/en/)
|
- Third Party F-Droid Repos
|
||||||
- [Signal](https://signal.org/)
|
- [IzzyOnDroid](https://android.izzysoft.de/)
|
||||||
- [SourceForge](https://sourceforge.net/)
|
- [SourceForge](https://sourceforge.net/)
|
||||||
- [SourceHut](https://git.sr.ht/)
|
- [SourceHut](https://git.sr.ht/)
|
||||||
- [Aptoide](https://aptoide.com/)
|
- Other - General:
|
||||||
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
- [APKPure](https://apkpure.com/)
|
||||||
- [APKPure](https://apkpure.com/)
|
- [Aptoide](https://aptoide.com/)
|
||||||
- [Huawei AppGallery](https://appgallery.huawei.com/)
|
- [Uptodown](https://uptodown.com/)
|
||||||
- Third Party F-Droid Repos
|
- [APKMirror](https://apkmirror.com/) (Track-Only)
|
||||||
- Jenkins Jobs
|
- [Huawei AppGallery](https://appgallery.huawei.com/)
|
||||||
- [Steam](https://store.steampowered.com/mobile)
|
- Jenkins Jobs
|
||||||
- [Telegram App](https://telegram.org)
|
- Open Source - App-Specific:
|
||||||
- [Neutron Code](https://neutroncode.com)
|
- [Mullvad](https://mullvad.net/en/)
|
||||||
- "HTML" (Fallback)
|
- [Signal](https://signal.org/)
|
||||||
- Any other URL that returns an HTML page with links to APK files (if multiple, the last file alphabetically is picked)
|
- [VLC](https://videolan.org/)
|
||||||
|
- Other - App-Specific:
|
||||||
|
- [Telegram App](https://telegram.org)
|
||||||
|
- [Steam Mobile Apps](https://store.steampowered.com/mobile)
|
||||||
|
- [Neutron Code](https://neutroncode.com)
|
||||||
|
- "HTML" (Fallback): Any other URL that returns an HTML page with links to APK files
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@@ -3,6 +3,21 @@ import 'package:html/parser.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 APKPure extends AppSource {
|
class APKPure extends AppSource {
|
||||||
APKPure() {
|
APKPure() {
|
||||||
host = 'apkpure.com';
|
host = 'apkpure.com';
|
||||||
@@ -47,17 +62,7 @@ class APKPure extends AppSource {
|
|||||||
}
|
}
|
||||||
String? dateString =
|
String? dateString =
|
||||||
html.querySelector('span.info-other span.date')?.text.trim();
|
html.querySelector('span.info-other span.date')?.text.trim();
|
||||||
DateTime? releaseDate;
|
DateTime? releaseDate = parseDateTimeMMMddCommayyyy(dateString);
|
||||||
try {
|
|
||||||
releaseDate = dateString != null
|
|
||||||
? DateFormat('MMM dd, yyyy').parse(dateString)
|
|
||||||
: null;
|
|
||||||
releaseDate = dateString != null && releaseDate == null
|
|
||||||
? DateFormat('MMMM dd, yyyy').parse(dateString)
|
|
||||||
: null;
|
|
||||||
} catch (err) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
|
String type = html.querySelector('a.info-tag')?.text.trim() ?? 'APK';
|
||||||
List<MapEntry<String, String>> apkUrls = [
|
List<MapEntry<String, String>> apkUrls = [
|
||||||
MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest')
|
MapEntry('$appId.apk', 'https://d.$host/b/$type/$appId?version=latest')
|
||||||
@@ -70,11 +75,13 @@ class APKPure extends AppSource {
|
|||||||
Uri.parse(standardUrl).pathSegments.reversed.last;
|
Uri.parse(standardUrl).pathSegments.reversed.last;
|
||||||
String appName =
|
String appName =
|
||||||
html.querySelector('h1.info-title')?.text.trim() ?? appId;
|
html.querySelector('h1.info-title')?.text.trim() ?? appId;
|
||||||
String? changeLog = htmlChangelog.querySelector("div.whats-new-info p:not(.date)")?.innerHtml
|
String? changeLog = htmlChangelog
|
||||||
.trim().replaceAll("<br>", " \n");
|
.querySelector("div.whats-new-info p:not(.date)")
|
||||||
|
?.innerHtml
|
||||||
|
.trim()
|
||||||
|
.replaceAll("<br>", " \n");
|
||||||
return APKDetails(version, apkUrls, AppNames(author, appName),
|
return APKDetails(version, apkUrls, AppNames(author, appName),
|
||||||
releaseDate: releaseDate,
|
releaseDate: releaseDate, changeLog: changeLog);
|
||||||
changeLog: changeLog);
|
|
||||||
} else {
|
} else {
|
||||||
throw getObtainiumHttpError(res);
|
throw getObtainiumHttpError(res);
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:html/parser.dart';
|
|
||||||
import 'package:http/http.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';
|
||||||
|
|
||||||
@@ -75,34 +73,4 @@ class Aptoide extends AppSource {
|
|||||||
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
|
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
|
||||||
releaseDate: relDate);
|
releaseDate: relDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Map<String, List<String>>> search(String query,
|
|
||||||
{Map<String, dynamic> querySettings = const {}}) async {
|
|
||||||
Response res = await sourceRequest(
|
|
||||||
'https://search.$host/?q=${Uri.encodeQueryComponent(query)}');
|
|
||||||
if (res.statusCode == 200) {
|
|
||||||
Map<String, List<String>> urlsWithDescriptions = {};
|
|
||||||
parse(res.body).querySelectorAll('.package-header').forEach((e) {
|
|
||||||
String? url = e.attributes['href'];
|
|
||||||
if (url != null) {
|
|
||||||
try {
|
|
||||||
standardizeUrl(url);
|
|
||||||
} catch (e) {
|
|
||||||
url = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (url != null) {
|
|
||||||
urlsWithDescriptions[url] = [
|
|
||||||
e.querySelector('.package-name')?.text.trim() ?? '',
|
|
||||||
e.querySelector('.package-summary')?.text.trim() ??
|
|
||||||
tr('noDescription')
|
|
||||||
];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return urlsWithDescriptions;
|
|
||||||
} else {
|
|
||||||
throw getObtainiumHttpError(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
82
lib/app_sources/uptodown.dart
Normal file
82
lib/app_sources/uptodown.dart
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
class Uptodown extends AppSource {
|
||||||
|
Uptodown() {
|
||||||
|
host = 'uptodown.com';
|
||||||
|
allowSubDomains = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String sourceSpecificStandardizeURL(String url) {
|
||||||
|
RegExp standardUrlRegEx = RegExp('^https?://([^\\.]+\\.){2,}$host');
|
||||||
|
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
|
||||||
|
if (match == null) {
|
||||||
|
throw InvalidURLError(name);
|
||||||
|
}
|
||||||
|
return '${url.substring(0, match.end)}/android/download';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> tryInferringAppId(String standardUrl,
|
||||||
|
{Map<String, dynamic> additionalSettings = const {}}) async {
|
||||||
|
return (await getAppDetailsFromPage(standardUrl))['appId'];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String?>> getAppDetailsFromPage(String standardUrl) async {
|
||||||
|
var res = await sourceRequest(standardUrl);
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw getObtainiumHttpError(res);
|
||||||
|
}
|
||||||
|
var html = parse(res.body);
|
||||||
|
String? version = html.querySelector('div.version')?.innerHtml;
|
||||||
|
String? apkUrl =
|
||||||
|
html.querySelector('#detail-download-button')?.attributes['data-url'];
|
||||||
|
String? name = html.querySelector('#detail-app-name')?.innerHtml;
|
||||||
|
String? author = html.querySelector('#author-link')?.innerHtml;
|
||||||
|
var detailElements = html.querySelectorAll('#technical-information td');
|
||||||
|
String? appId = (detailElements.elementAtOrNull(2))?.innerHtml;
|
||||||
|
String? dateStr = (detailElements.elementAtOrNull(29))?.innerHtml;
|
||||||
|
return Map.fromEntries([
|
||||||
|
MapEntry('version', version),
|
||||||
|
MapEntry('apkUrl', apkUrl),
|
||||||
|
MapEntry('appId', appId),
|
||||||
|
MapEntry('name', name),
|
||||||
|
MapEntry('author', author),
|
||||||
|
MapEntry('dateStr', dateStr)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<APKDetails> getLatestAPKDetails(
|
||||||
|
String standardUrl,
|
||||||
|
Map<String, dynamic> additionalSettings,
|
||||||
|
) async {
|
||||||
|
var appDetails = await getAppDetailsFromPage(standardUrl);
|
||||||
|
var version = appDetails['version'];
|
||||||
|
var apkUrl = appDetails['apkUrl'];
|
||||||
|
var appId = appDetails['appId'];
|
||||||
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
if (apkUrl == null) {
|
||||||
|
throw NoAPKError();
|
||||||
|
}
|
||||||
|
if (appId == null) {
|
||||||
|
throw NoReleasesError();
|
||||||
|
}
|
||||||
|
String appName = appDetails['name'] ?? tr('app');
|
||||||
|
String author = appDetails['author'] ?? name;
|
||||||
|
String? dateStr = appDetails['dateStr'];
|
||||||
|
DateTime? relDate;
|
||||||
|
if (dateStr != null) {
|
||||||
|
relDate = parseDateTimeMMMddCommayyyy(dateStr);
|
||||||
|
}
|
||||||
|
return APKDetails(
|
||||||
|
version, getApkUrlsFromUrls([apkUrl]), AppNames(author, appName),
|
||||||
|
releaseDate: relDate);
|
||||||
|
}
|
||||||
|
}
|
@@ -26,6 +26,7 @@ import 'package:obtainium/app_sources/sourceforge.dart';
|
|||||||
import 'package:obtainium/app_sources/sourcehut.dart';
|
import 'package:obtainium/app_sources/sourcehut.dart';
|
||||||
import 'package:obtainium/app_sources/steammobile.dart';
|
import 'package:obtainium/app_sources/steammobile.dart';
|
||||||
import 'package:obtainium/app_sources/telegramapp.dart';
|
import 'package:obtainium/app_sources/telegramapp.dart';
|
||||||
|
import 'package:obtainium/app_sources/uptodown.dart';
|
||||||
import 'package:obtainium/app_sources/vlc.dart';
|
import 'package:obtainium/app_sources/vlc.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';
|
||||||
@@ -527,15 +528,16 @@ class SourceProvider {
|
|||||||
GitLab(),
|
GitLab(),
|
||||||
Codeberg(),
|
Codeberg(),
|
||||||
FDroid(),
|
FDroid(),
|
||||||
IzzyOnDroid(),
|
|
||||||
FDroidRepo(),
|
FDroidRepo(),
|
||||||
Jenkins(),
|
IzzyOnDroid(),
|
||||||
SourceForge(),
|
SourceForge(),
|
||||||
SourceHut(),
|
SourceHut(),
|
||||||
Aptoide(),
|
|
||||||
APKMirror(),
|
|
||||||
APKPure(),
|
APKPure(),
|
||||||
|
Aptoide(),
|
||||||
|
Uptodown(),
|
||||||
|
APKMirror(),
|
||||||
HuaweiAppGallery(),
|
HuaweiAppGallery(),
|
||||||
|
Jenkins(),
|
||||||
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
|
// APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
|
||||||
Mullvad(),
|
Mullvad(),
|
||||||
Signal(),
|
Signal(),
|
||||||
|
@@ -20,7 +20,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
version: 0.14.10+202 # When changing this, update the tag in main() accordingly
|
version: 0.14.10+202 # When changing this, update the tag in main() accordingly
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.18.2 <3.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
|
Reference in New Issue
Block a user