diff --git a/README.md b/README.md index 229979b..2832b4d 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Currently supported App sources: - [CoolApk](https://coolapk.com/) - [vivo App Store (CN)](https://h5.appstore.vivo.com.cn/) - [RuStore](https://rustore.ru/) + - [Farsroid](https://www.farsroid.com) - Jenkins Jobs - [APKMirror](https://apkmirror.com/) (Track-Only) - Other - App-Specific: diff --git a/lib/app_sources/farsroid.dart b/lib/app_sources/farsroid.dart new file mode 100644 index 0000000..82754d0 --- /dev/null +++ b/lib/app_sources/farsroid.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; + +import 'package:html/parser.dart'; +import 'package:obtainium/app_sources/html.dart'; +import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/providers/source_provider.dart'; + +class Farsroid extends AppSource { + Farsroid() { + hosts = ['farsroid.com']; + name = 'Farsroid'; + } + + @override + String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { + RegExp standardUrlRegEx = RegExp( + '^https?://([^\\.]+\\.)${getSourceRegex(hosts)}/[^/]+', + caseSensitive: false, + ); + RegExpMatch? match = standardUrlRegEx.firstMatch(url); + if (match == null) { + throw InvalidURLError(name); + } + return match.group(0)!; + } + + @override + Future getLatestAPKDetails( + String standardUrl, + Map additionalSettings, + ) async { + String appName = Uri.parse(standardUrl).pathSegments.last; + + var res = await sourceRequest(standardUrl, additionalSettings); + if (res.statusCode != 200) { + throw getObtainiumHttpError(res); + } + var html = parse(res.body); + var dlinks = html.querySelectorAll('.download-links'); + if (dlinks.isEmpty) { + throw NoReleasesError(); + } + var postId = dlinks.first.attributes['data-post-id'] ?? ''; + var version = dlinks.first.attributes['data-post-version'] ?? ''; + + if (postId.isEmpty || version.isEmpty) { + throw NoVersionError(); + } + + var res2 = await sourceRequest( + Uri.encodeFull( + 'https://${hosts[0]}/api/download-box/?post_id=$postId&post_version=$version', + ), + additionalSettings, + ); + var html2 = jsonDecode(res2.body)?['data']?['content'] as String? ?? ''; + if (html2.isEmpty) { + throw NoAPKError(); + } + var apkLinks = + (await grabLinksCommon(html2, res2.request!.url, additionalSettings)) + .map((l) => MapEntry(Uri.parse(l.key).pathSegments.last, l.key)) + .where( + (l) => l.key.toLowerCase().startsWith( + '$appName-$version'.toLowerCase(), + ), + ) + .toList(); + + if (apkLinks.isEmpty) { + throw NoAPKError(); + } + + return APKDetails(version, apkLinks, AppNames(name, appName)); + } +} diff --git a/lib/app_sources/html.dart b/lib/app_sources/html.dart index 437cb6f..08c070e 100644 --- a/lib/app_sources/html.dart +++ b/lib/app_sources/html.dart @@ -113,14 +113,23 @@ List> getLinksInLines(String lines) => // Given an HTTP response, grab some links according to the common additional settings // (those that apply to intermediate and final steps) -Future>> grabLinksCommon( +Future>> grabLinksCommonFromRes( Response res, Map additionalSettings, ) async { if (res.statusCode != 200) { throw getObtainiumHttpError(res); } - var html = parse(res.body); + return grabLinksCommon(res.body, res.request!.url, additionalSettings); +} + +// Note keys are URLs, values are filenames (opposite to the AppSource apkUrls) +Future>> grabLinksCommon( + String rawBody, + Uri reqUrl, + Map additionalSettings, +) async { + var html = parse(rawBody); List> allLinks = html .querySelectorAll('a') .map( @@ -132,21 +141,21 @@ Future>> grabLinksCommon( ), ) .where((element) => element.key.isNotEmpty) - .map((e) => MapEntry(ensureAbsoluteUrl(e.key, res.request!.url), e.value)) + .map((e) => MapEntry(ensureAbsoluteUrl(e.key, reqUrl), e.value)) .toList(); if (allLinks.isEmpty) { - allLinks = getLinksInLines(res.body); + allLinks = getLinksInLines(rawBody); } if (allLinks.isEmpty) { // Getting desperate try { - var jsonStrings = collectAllStringsFromJSONObject(jsonDecode(res.body)); + var jsonStrings = collectAllStringsFromJSONObject(jsonDecode(rawBody)); allLinks = getLinksInLines(jsonStrings.join('\n')); if (allLinks.isEmpty) { allLinks = getLinksInLines( jsonStrings .map((l) { - return ensureAbsoluteUrl(l, res.request!.url); + return ensureAbsoluteUrl(l, reqUrl); }) .join('\n'), ); @@ -368,7 +377,7 @@ class HTML extends AppSource { .where((l) => l['customLinkFilterRegex'].isNotEmpty == true) .toList(); for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) { - var intLinks = await grabLinksCommon( + var intLinks = await grabLinksCommonFromRes( await sourceRequest(currentUrl, additionalSettings), additionalSettings['intermediateLink'][i], ); @@ -392,7 +401,7 @@ class HTML extends AppSource { .join('\n') .split('\n') .join('\\n'); - links = await grabLinksCommon(res, additionalSettings); + links = await grabLinksCommonFromRes(res, additionalSettings); links = filterApks( links, additionalSettings['apkFilterRegEx'], diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index f14d988..d9c1b69 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -16,6 +16,7 @@ import 'package:obtainium/app_sources/aptoide.dart'; import 'package:obtainium/app_sources/codeberg.dart'; import 'package:obtainium/app_sources/coolapk.dart'; import 'package:obtainium/app_sources/directAPKLink.dart'; +import 'package:obtainium/app_sources/farsroid.dart'; import 'package:obtainium/app_sources/fdroid.dart'; import 'package:obtainium/app_sources/fdroidrepo.dart'; import 'package:obtainium/app_sources/github.dart'; @@ -63,11 +64,13 @@ class APKDetails { }); } -List> stringMapListTo2DList(List> mapList) => - mapList.map((e) => [e.key, e.value]).toList(); +List> stringMapListTo2DList( + List> mapList, +) => mapList.map((e) => [e.key, e.value]).toList(); -List> assumed2DlistToStringMapList(List arr) => - arr.map((e) => MapEntry(e[0] as String, e[1] as String)).toList(); +List> assumed2DlistToStringMapList( + List arr, +) => arr.map((e) => MapEntry(e[0] as String, e[1] as String)).toList(); // App JSON schema has changed multiple times over the many versions of Obtainium // This function takes an App JSON and modifies it if needed to conform to the latest (current) version @@ -1074,6 +1077,7 @@ class SourceProvider { Jenkins(), APKMirror(), RuStore(), + Farsroid(), TelegramApp(), NeutronCode(), DirectAPKLink(),