From 849a5651773014c5f2d33645a777fec6e000f088 Mon Sep 17 00:00:00 2001 From: iKirby <6316115+iKirby@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:01:11 +0800 Subject: [PATCH 1/7] Add support for vivo App Store --- README.md | 1 + assets/translations/en.json | 1 + assets/translations/zh.json | 1 + .../android/en-US/full_description.txt | 1 + .../metadata/android/ru/full_description.txt | 1 + lib/app_sources/vivoappstore.dart | 105 ++++++++++++++++++ lib/providers/source_provider.dart | 2 + 7 files changed, 112 insertions(+) create mode 100644 lib/app_sources/vivoappstore.dart diff --git a/README.md b/README.md index 6518041..686b646 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Currently supported App sources: - [Huawei AppGallery](https://appgallery.huawei.com/) - [Tencent App Store](https://sj.qq.com/) - [CoolApk](https://coolapk.com/) + - [vivo App Store](https://h5.appstore.vivo.com.cn/) - [RuStore](https://rustore.ru/) - Jenkins Jobs - [APKMirror](https://apkmirror.com/) (Track-Only) diff --git a/assets/translations/en.json b/assets/translations/en.json index acc31b7..d5c431e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -321,6 +321,7 @@ "refreshBeforeDownload": "Refresh app details before download", "tencentAppStore": "Tencent App Store", "coolApk": "CoolApk", + "vivoAppStore": "vivo App Store", "name": "Name", "smartname": "Name (smart)", "sortMethod": "Sort method", diff --git a/assets/translations/zh.json b/assets/translations/zh.json index 4e617a0..bb36a17 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -321,6 +321,7 @@ "refreshBeforeDownload": "下载前刷新应用程序详细信息", "tencentAppStore": "腾讯应用宝", "coolApk": "酷安", + "vivoAppStore": "vivo 应用商店", "name": "名称", "smartname": "姓名(智能)", "sortMethod": "排序方法", diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index d620444..7fed3cf 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -26,6 +26,7 @@
  • Huawei AppGallery
  • Tencent App Store
  • CoolApk
  • +
  • vivo App Store
  • Jenkins Jobs
  • RuStore
  • diff --git a/fastlane/metadata/android/ru/full_description.txt b/fastlane/metadata/android/ru/full_description.txt index 7249abd..d67a272 100644 --- a/fastlane/metadata/android/ru/full_description.txt +++ b/fastlane/metadata/android/ru/full_description.txt @@ -26,6 +26,7 @@
  • Huawei AppGallery
  • Tencent App Store
  • CoolApk
  • +
  • vivo App Store
  • Jenkins Jobs
  • RuStore
  • diff --git a/lib/app_sources/vivoappstore.dart b/lib/app_sources/vivoappstore.dart new file mode 100644 index 0000000..6006746 --- /dev/null +++ b/lib/app_sources/vivoappstore.dart @@ -0,0 +1,105 @@ +import 'dart:convert'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/providers/source_provider.dart'; + +class VivoAppStore extends AppSource { + static const appDetailUrl = + 'https://h5coml.vivo.com.cn/h5coml/appdetail_h5/browser_v2/index.html?appId='; + + VivoAppStore() { + name = tr('vivoAppStore'); + hosts = ['h5.appstore.vivo.com.cn', 'h5coml.vivo.com.cn']; + naiveStandardVersionDetection = true; + canSearch = true; + } + + @override + String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { + var vivoAppId = + Uri.parse(url.replaceAll('/#', '')).queryParameters['appId']; + if (vivoAppId == null) { + throw InvalidURLError(name); + } + return '$appDetailUrl$vivoAppId'; + } + + @override + Future tryInferringAppId(String standardUrl, + {Map additionalSettings = const {}}) async { + var json = await getDetailJson(standardUrl, additionalSettings); + return json['package_name']; + } + + @override + Future getLatestAPKDetails( + String standardUrl, Map additionalSettings) async { + var json = await getDetailJson(standardUrl, additionalSettings); + var appName = json['title_zh'].toString(); + var packageName = json['package_name'].toString(); + var versionName = json['version_name'].toString(); + var versionCode = json['version_code'].toString(); + var developer = json['developer'].toString(); + var uploadTime = json['upload_time'].toString(); + var apkUrl = json['download_url'].toString(); + var apkName = '${packageName}_$versionCode.apk'; + return APKDetails( + versionName, [MapEntry(apkName, apkUrl)], AppNames(developer, appName), + releaseDate: DateTime.parse(uploadTime)); + } + + @override + Future?> getRequestHeaders( + Map additionalSettings, + {bool forAPKDownload = false}) async { + return { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36' + }; + } + + @override + Future>> search(String query, + {Map querySettings = const {}}) async { + var apiBaseUrl = + 'https://h5-api.appstore.vivo.com.cn/h5appstore/search/result-list?app_version=2100&page_index=1&apps_per_page=20&target=local&cfrom=2&key='; + var searchUrl = '$apiBaseUrl${Uri.encodeQueryComponent(query)}'; + var response = await sourceRequest(searchUrl, {}); + if (response.statusCode != 200) { + throw getObtainiumHttpError(response); + } + var json = jsonDecode(response.body); + if (json['code'] != 0 || !json['data']['appSearchResponse']['result']) { + throw NoReleasesError(); + } + Map> results = {}; + var resultsJson = json['data']['appSearchResponse']['value']; + for (var item in (resultsJson as List)) { + results['$appDetailUrl${item['id']}'] = [ + item['title_zh'].toString(), + item['developer'].toString() + ]; + } + return results; + } + + Future> getDetailJson( + String standardUrl, Map additionalSettings) async { + var vivoAppId = Uri.parse(standardUrl).queryParameters['appId']; + var apiBaseUrl = 'https://h5-api.appstore.vivo.com.cn/detail/'; + var params = '?frompage=messageh5&app_version=2100'; + + var detailUrl = '$apiBaseUrl$vivoAppId$params'; + var response = await sourceRequest(detailUrl, additionalSettings); + if (response.statusCode != 200) { + throw getObtainiumHttpError(response); + } + + var json = jsonDecode(response.body); + if (json['id'] == null) { + throw NoReleasesError(); + } + return json; + } +} diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 72bc931..dfa423d 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -31,6 +31,7 @@ import 'package:obtainium/app_sources/sourcehut.dart'; import 'package:obtainium/app_sources/telegramapp.dart'; import 'package:obtainium/app_sources/tencent.dart'; import 'package:obtainium/app_sources/uptodown.dart'; +import 'package:obtainium/app_sources/vivoappstore.dart'; import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/mass_app_sources/githubstars.dart'; @@ -954,6 +955,7 @@ class SourceProvider { HuaweiAppGallery(), Tencent(), CoolApk(), + VivoAppStore(), Jenkins(), APKMirror(), RuStore(), From 8d062f029605c4288358619e0fbd1233e07e5ff9 Mon Sep 17 00:00:00 2001 From: iKirby <6316115+iKirby@users.noreply.github.com> Date: Tue, 6 May 2025 15:52:14 +0800 Subject: [PATCH 2/7] Remove unnecessary custom header --- lib/app_sources/vivoappstore.dart | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/app_sources/vivoappstore.dart b/lib/app_sources/vivoappstore.dart index 6006746..b68b694 100644 --- a/lib/app_sources/vivoappstore.dart +++ b/lib/app_sources/vivoappstore.dart @@ -49,16 +49,6 @@ class VivoAppStore extends AppSource { releaseDate: DateTime.parse(uploadTime)); } - @override - Future?> getRequestHeaders( - Map additionalSettings, - {bool forAPKDownload = false}) async { - return { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36' - }; - } - @override Future>> search(String query, {Map querySettings = const {}}) async { From 7a7a6f78e98da9faf304d5433b3ea2aea302c869 Mon Sep 17 00:00:00 2001 From: iKirby <6316115+iKirby@users.noreply.github.com> Date: Tue, 6 May 2025 16:04:03 +0800 Subject: [PATCH 3/7] Indicate vivo App Store is for CN site only --- README.md | 2 +- assets/translations/en.json | 2 +- assets/translations/zh.json | 2 +- fastlane/metadata/android/en-US/full_description.txt | 2 +- fastlane/metadata/android/ru/full_description.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 686b646..b0b3bd9 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Currently supported App sources: - [Huawei AppGallery](https://appgallery.huawei.com/) - [Tencent App Store](https://sj.qq.com/) - [CoolApk](https://coolapk.com/) - - [vivo App Store](https://h5.appstore.vivo.com.cn/) + - [vivo App Store (CN)](https://h5.appstore.vivo.com.cn/) - [RuStore](https://rustore.ru/) - Jenkins Jobs - [APKMirror](https://apkmirror.com/) (Track-Only) diff --git a/assets/translations/en.json b/assets/translations/en.json index d5c431e..9ea7e44 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -321,7 +321,7 @@ "refreshBeforeDownload": "Refresh app details before download", "tencentAppStore": "Tencent App Store", "coolApk": "CoolApk", - "vivoAppStore": "vivo App Store", + "vivoAppStore": "vivo App Store (CN)", "name": "Name", "smartname": "Name (smart)", "sortMethod": "Sort method", diff --git a/assets/translations/zh.json b/assets/translations/zh.json index bb36a17..33e3782 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -321,7 +321,7 @@ "refreshBeforeDownload": "下载前刷新应用程序详细信息", "tencentAppStore": "腾讯应用宝", "coolApk": "酷安", - "vivoAppStore": "vivo 应用商店", + "vivoAppStore": "vivo 应用商店(中国)", "name": "名称", "smartname": "姓名(智能)", "sortMethod": "排序方法", diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 7fed3cf..ecdd345 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -26,7 +26,7 @@
  • Huawei AppGallery
  • Tencent App Store
  • CoolApk
  • -
  • vivo App Store
  • +
  • vivo App Store (CN)
  • Jenkins Jobs
  • RuStore
  • diff --git a/fastlane/metadata/android/ru/full_description.txt b/fastlane/metadata/android/ru/full_description.txt index d67a272..61e9331 100644 --- a/fastlane/metadata/android/ru/full_description.txt +++ b/fastlane/metadata/android/ru/full_description.txt @@ -26,7 +26,7 @@
  • Huawei AppGallery
  • Tencent App Store
  • CoolApk
  • -
  • vivo App Store
  • +
  • vivo App Store (CN)
  • Jenkins Jobs
  • RuStore
  • From 26bca6df64cc686e968880e578c9fbc3b2adc45c Mon Sep 17 00:00:00 2001 From: iKirby <6316115+iKirby@users.noreply.github.com> Date: Tue, 6 May 2025 16:18:31 +0800 Subject: [PATCH 4/7] Fix parsing vivo app id from URL when using source override --- lib/app_sources/vivoappstore.dart | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/app_sources/vivoappstore.dart b/lib/app_sources/vivoappstore.dart index b68b694..fbd512e 100644 --- a/lib/app_sources/vivoappstore.dart +++ b/lib/app_sources/vivoappstore.dart @@ -17,11 +17,7 @@ class VivoAppStore extends AppSource { @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { - var vivoAppId = - Uri.parse(url.replaceAll('/#', '')).queryParameters['appId']; - if (vivoAppId == null) { - throw InvalidURLError(name); - } + var vivoAppId = parseVivoAppId(url); return '$appDetailUrl$vivoAppId'; } @@ -76,20 +72,26 @@ class VivoAppStore extends AppSource { Future> getDetailJson( String standardUrl, Map additionalSettings) async { - var vivoAppId = Uri.parse(standardUrl).queryParameters['appId']; + var vivoAppId = parseVivoAppId(standardUrl); var apiBaseUrl = 'https://h5-api.appstore.vivo.com.cn/detail/'; var params = '?frompage=messageh5&app_version=2100'; - var detailUrl = '$apiBaseUrl$vivoAppId$params'; var response = await sourceRequest(detailUrl, additionalSettings); if (response.statusCode != 200) { throw getObtainiumHttpError(response); } - var json = jsonDecode(response.body); if (json['id'] == null) { throw NoReleasesError(); } return json; } + + String parseVivoAppId(String url) { + var appId = Uri.parse(url.replaceAll('/#', '')).queryParameters['appId']; + if (appId == null) { + throw InvalidURLError(name); + } + return appId; + } } From 9ce74beda51181e4400f658612e96f21d2f1df10 Mon Sep 17 00:00:00 2001 From: iKirby <6316115+iKirby@users.noreply.github.com> Date: Tue, 6 May 2025 16:56:13 +0800 Subject: [PATCH 5/7] Add appId empty check --- lib/app_sources/vivoappstore.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app_sources/vivoappstore.dart b/lib/app_sources/vivoappstore.dart index fbd512e..9b55ce2 100644 --- a/lib/app_sources/vivoappstore.dart +++ b/lib/app_sources/vivoappstore.dart @@ -89,7 +89,7 @@ class VivoAppStore extends AppSource { String parseVivoAppId(String url) { var appId = Uri.parse(url.replaceAll('/#', '')).queryParameters['appId']; - if (appId == null) { + if (appId == null || appId.isEmpty) { throw InvalidURLError(name); } return appId; From 1d69e42949dc3916dc81857a248239feafa29606 Mon Sep 17 00:00:00 2001 From: Michael Wood Date: Mon, 12 May 2025 13:18:16 +0200 Subject: [PATCH 6/7] Add allowOverride app source option --- lib/app_sources/coolapk.dart | 3 ++- lib/pages/add_app.dart | 10 ++++++++-- lib/providers/source_provider.dart | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/app_sources/coolapk.dart b/lib/app_sources/coolapk.dart index 01027da..741ec26 100644 --- a/lib/app_sources/coolapk.dart +++ b/lib/app_sources/coolapk.dart @@ -13,6 +13,7 @@ class CoolApk extends AppSource { hosts = ['www.coolapk.com', 'api2.coolapk.com']; allowSubDomains = true; naiveStandardVersionDetection = true; + allowOverride = false; } @override @@ -170,4 +171,4 @@ class CoolApk extends AppSource { return {'deviceCode': deviceCode, 'token': finalToken}; } -} \ No newline at end of file +} diff --git a/lib/pages/add_app.dart b/lib/pages/add_app.dart index 41d9f9c..327819a 100644 --- a/lib/pages/add_app.dart +++ b/lib/pages/add_app.dart @@ -407,8 +407,14 @@ class AddAppPageState extends State { defaultValue: pickedSourceOverride ?? '', [ MapEntry('', tr('none')), - ...sourceProvider.sources.map( - (s) => MapEntry(s.runtimeType.toString(), s.name)) + ...sourceProvider.sources + .where((s) => + s.allowOverride || + (pickedSource != null && + pickedSource.runtimeType == + s.runtimeType)) + .map((s) => + MapEntry(s.runtimeType.toString(), s.name)) ], label: tr('overrideSource')) ] diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 72bc931..a9852d6 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -586,6 +586,7 @@ abstract class AppSource { bool appIdInferIsOptional = false; bool allowSubDomains = false; bool naiveStandardVersionDetection = false; + bool allowOverride = true; bool neverAutoSelect = false; bool showReleaseDateAsVersionToggle = false; bool versionDetectionDisallowed = false; From b2742dd904c41b4acb52702ad3cd616f9e117adb Mon Sep 17 00:00:00 2001 From: bernikr Date: Wed, 14 May 2025 16:17:22 +0200 Subject: [PATCH 7/7] allow apkpure links for apks with no variant listt --- lib/app_sources/apkpure.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/app_sources/apkpure.dart b/lib/app_sources/apkpure.dart index 982c409..8894422 100644 --- a/lib/app_sources/apkpure.dart +++ b/lib/app_sources/apkpure.dart @@ -128,7 +128,21 @@ class APKPure extends AppSource { .toList() ?? []; if (apkUrls.isEmpty) { - throw NoAPKError(); + 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;