Added farsroid.com as a source (#2435)

This commit is contained in:
Imran Remtulla
2025-08-01 17:50:16 -04:00
parent 8f9978aadd
commit 89d853a948
4 changed files with 102 additions and 12 deletions

View File

@@ -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:

View File

@@ -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<APKDetails> getLatestAPKDetails(
String standardUrl,
Map<String, dynamic> 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));
}
}

View File

@@ -113,14 +113,23 @@ List<MapEntry<String, String>> 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<List<MapEntry<String, String>>> grabLinksCommon(
Future<List<MapEntry<String, String>>> grabLinksCommonFromRes(
Response res,
Map<String, dynamic> 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<List<MapEntry<String, String>>> grabLinksCommon(
String rawBody,
Uri reqUrl,
Map<String, dynamic> additionalSettings,
) async {
var html = parse(rawBody);
List<MapEntry<String, String>> allLinks = html
.querySelectorAll('a')
.map(
@@ -132,21 +141,21 @@ Future<List<MapEntry<String, String>>> 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'],

View File

@@ -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<List<String>> stringMapListTo2DList(List<MapEntry<String, String>> mapList) =>
mapList.map((e) => [e.key, e.value]).toList();
List<List<String>> stringMapListTo2DList(
List<MapEntry<String, String>> mapList,
) => mapList.map((e) => [e.key, e.value]).toList();
List<MapEntry<String, String>> assumed2DlistToStringMapList(List<dynamic> arr) =>
arr.map((e) => MapEntry(e[0] as String, e[1] as String)).toList();
List<MapEntry<String, String>> assumed2DlistToStringMapList(
List<dynamic> 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(),