diff --git a/README.md b/README.md index 2b4b120..ddd731b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Currently supported App sources: - Third Party F-Droid Repos - Any URLs ending with `/fdroid/`, where `` can be anything - most often `repo` - [Steam](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 (if multiple, the last file alphabetically is picked) diff --git a/lib/app_sources/neutroncode.dart b/lib/app_sources/neutroncode.dart new file mode 100644 index 0000000..0fdbc6c --- /dev/null +++ b/lib/app_sources/neutroncode.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; +import 'package:html/parser.dart'; +import 'package:http/http.dart'; +import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/providers/source_provider.dart'; + +class NeutronCode extends AppSource { + NeutronCode() { + host = 'neutroncode.com'; + } + + @override + String standardizeURL(String url) { + RegExp standardUrlRegEx = RegExp('^https?://$host/downloads/file/[^/]+'); + RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); + if (match == null) { + throw InvalidURLError(name); + } + return url.substring(0, match.end); + } + + @override + String? changeLogPageFromStandardUrl(String standardUrl) => standardUrl; + + String monthNameToNumberString(String s) { + switch (s.toLowerCase()) { + case 'january': + return '01'; + case 'february': + return '02'; + case 'march': + return '03'; + case 'april': + return '04'; + case 'may': + return '05'; + case 'june': + return '06'; + case 'july': + return '07'; + case 'august': + return '08'; + case 'september': + return '09'; + case 'october': + return '10'; + case 'november': + return '11'; + case 'december': + return '12'; + default: + throw ArgumentError('Invalid month name: $s'); + } + } + + customDateParse(String dateString) { + List parts = dateString.split(' '); + if (parts.length != 3) { + return null; + } + String result = ''; + for (var s in parts.reversed) { + try { + try { + int.parse(s); + result += '$s-'; + } catch (e) { + result += '${monthNameToNumberString(s)}-'; + } + } catch (e) { + return null; + } + } + return result.substring(0, result.length - 1); + } + + @override + Future getLatestAPKDetails( + String standardUrl, + Map additionalSettings, + ) async { + Response res = await get(Uri.parse(standardUrl)); + if (res.statusCode == 200) { + var http = parse(res.body); + var name = http.querySelector('.pd-title')?.innerHtml; + var filename = http.querySelector('.pd-filename .pd-float')?.innerHtml; + if (filename == null) { + throw NoReleasesError(); + } + var version = + http.querySelector('.pd-version-txt')?.nextElementSibling?.innerHtml; + if (version == null) { + throw NoVersionError(); + } + String? apkUrl = 'https://$host/download/$filename'; + var dateStringOriginal = + http.querySelector('.pd-date-txt')?.nextElementSibling?.innerHtml; + var dateString = dateStringOriginal != null + ? (customDateParse(dateStringOriginal)) + : null; + + return APKDetails(version, [apkUrl], + AppNames(runtimeType.toString(), name ?? standardUrl.split('/').last), + releaseDate: dateString != null ? DateTime.parse(dateString) : null); + } else { + throw getObtainiumHttpError(res); + } + } +} diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 8cead1e..5b51c7f 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -15,6 +15,7 @@ import 'package:obtainium/app_sources/gitlab.dart'; import 'package:obtainium/app_sources/izzyondroid.dart'; import 'package:obtainium/app_sources/html.dart'; import 'package:obtainium/app_sources/mullvad.dart'; +import 'package:obtainium/app_sources/neutroncode.dart'; import 'package:obtainium/app_sources/signal.dart'; import 'package:obtainium/app_sources/sourceforge.dart'; import 'package:obtainium/app_sources/steammobile.dart'; @@ -338,6 +339,7 @@ class SourceProvider { APKMirror(), FDroidRepo(), SteamMobile(), + NeutronCode(), HTML() // This should ALWAYS be the last option as they are tried in order ];