diff --git a/lib/app_sources/codeberg.dart b/lib/app_sources/codeberg.dart index 5787485..dc71fd6 100644 --- a/lib/app_sources/codeberg.dart +++ b/lib/app_sources/codeberg.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:easy_localization/easy_localization.dart'; import 'package:http/http.dart'; +import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/providers/source_provider.dart'; @@ -35,6 +36,8 @@ class Codeberg extends AppSource { canSearch = true; } + var gh = GitHub(); + @override String standardizeURL(String url) { RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+'); @@ -54,80 +57,10 @@ class Codeberg extends AppSource { String standardUrl, Map additionalSettings, ) async { - bool includePrereleases = additionalSettings['includePrereleases'] == true; - bool fallbackToOlderReleases = - additionalSettings['fallbackToOlderReleases'] == true; - String? regexFilter = - (additionalSettings['filterReleaseTitlesByRegEx'] as String?) - ?.isNotEmpty == - true - ? additionalSettings['filterReleaseTitlesByRegEx'] - : null; - Response res = await get(Uri.parse( - 'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100')); - if (res.statusCode == 200) { - var releases = jsonDecode(res.body) as List; - - List> getReleaseAPKUrls(dynamic release) => - (release['assets'] as List?) - ?.map((e) { - return e['name'] != null && e['browser_download_url'] != null - ? MapEntry(e['name'] as String, - e['browser_download_url'] as String) - : const MapEntry('', ''); - }) - .where((element) => element.key.toLowerCase().endsWith('.apk')) - .toList() ?? - []; - - dynamic targetRelease; - var prerrelsSkipped = 0; - for (int i = 0; i < releases.length; i++) { - if (!fallbackToOlderReleases && i > prerrelsSkipped) break; - if (!includePrereleases && releases[i]['prerelease'] == true) { - prerrelsSkipped++; - continue; - } - if (releases[i]['draft'] == true) { - // Draft releases not supported - } - var nameToFilter = releases[i]['name'] as String?; - if (nameToFilter == null || nameToFilter.trim().isEmpty) { - // Some leave titles empty so tag is used - nameToFilter = releases[i]['tag_name'] as String; - } - if (regexFilter != null && - !RegExp(regexFilter).hasMatch(nameToFilter.trim())) { - continue; - } - var apkUrls = getReleaseAPKUrls(releases[i]); - if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) { - continue; - } - targetRelease = releases[i]; - targetRelease['apkUrls'] = apkUrls; - break; - } - if (targetRelease == null) { - throw NoReleasesError(); - } - String? version = targetRelease['tag_name']; - DateTime? releaseDate = targetRelease['published_at'] != null - ? DateTime.parse(targetRelease['published_at']) - : null; - if (version == null) { - throw NoVersionError(); - } - var changeLog = targetRelease['body'].toString(); - return APKDetails( - version, - targetRelease['apkUrls'] as List>, - getAppNames(standardUrl), - releaseDate: releaseDate, - changeLog: changeLog.isEmpty ? null : changeLog); - } else { - throw getObtainiumHttpError(res); - } + return gh.getLatestAPKDetailsCommon( + 'https://$host/api/v1/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100', + standardUrl, + additionalSettings); } AppNames getAppNames(String standardUrl) { @@ -138,20 +71,9 @@ class Codeberg extends AppSource { @override Future> search(String query) async { - Response res = await get(Uri.parse( - 'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100')); - if (res.statusCode == 200) { - Map urlsWithDescriptions = {}; - for (var e in (jsonDecode(res.body)['data'] as List)) { - urlsWithDescriptions.addAll({ - e['html_url'] as String: e['description'] != null - ? e['description'] as String - : tr('noDescription') - }); - } - return urlsWithDescriptions; - } else { - throw getObtainiumHttpError(res); - } + return gh.searchCommon( + query, + 'https://$host/api/v1/repos/search?q=${Uri.encodeQueryComponent(query)}&limit=100', + 'data'); } } diff --git a/lib/app_sources/github.dart b/lib/app_sources/github.dart index 4741fd4..5b6f2cf 100644 --- a/lib/app_sources/github.dart +++ b/lib/app_sources/github.dart @@ -96,11 +96,9 @@ class GitHub extends AppSource { String? changeLogPageFromStandardUrl(String standardUrl) => '$standardUrl/releases'; - @override - Future getLatestAPKDetails( - String standardUrl, - Map additionalSettings, - ) async { + Future getLatestAPKDetailsCommon(String requestUrl, + String standardUrl, Map additionalSettings, + {Function(Response)? onHttpErrorCode}) async { bool includePrereleases = additionalSettings['includePrereleases'] == true; bool fallbackToOlderReleases = additionalSettings['fallbackToOlderReleases'] == true; @@ -110,22 +108,40 @@ class GitHub extends AppSource { true ? additionalSettings['filterReleaseTitlesByRegEx'] : null; - Response res = await get(Uri.parse( - 'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100')); + Response res = await get(Uri.parse(requestUrl)); if (res.statusCode == 200) { var releases = jsonDecode(res.body) as List; - List getReleaseAPKUrls(dynamic release) => + List> getReleaseAPKUrls(dynamic release) => (release['assets'] as List?) ?.map((e) { - return e['browser_download_url'] != null - ? e['browser_download_url'] as String - : ''; + return e['name'] != null && e['browser_download_url'] != null + ? MapEntry(e['name'] as String, + e['browser_download_url'] as String) + : const MapEntry('', ''); }) - .where((element) => element.toLowerCase().endsWith('.apk')) + .where((element) => element.key.toLowerCase().endsWith('.apk')) .toList() ?? []; + DateTime? getReleaseDateFromRelease(dynamic rel) => + rel?['published_at'] != null + ? DateTime.parse(rel['published_at']) + : null; + releases.sort((a, b) { + // See #478 + if (a == b) { + return 0; + } else if (a == null) { + return -1; + } else if (b == null) { + return 1; + } else { + return getReleaseDateFromRelease(a)! + .compareTo(getReleaseDateFromRelease(b)!); + } + }); + releases = releases.reversed.toList(); dynamic targetRelease; var prerrelsSkipped = 0; for (int i = 0; i < releases.length; i++) { @@ -134,6 +150,10 @@ class GitHub extends AppSource { prerrelsSkipped++; continue; } + if (releases[i]['draft'] == true) { + // Draft releases not supported + continue; + } var nameToFilter = releases[i]['name'] as String?; if (nameToFilter == null || nameToFilter.trim().isEmpty) { // Some leave titles empty so tag is used @@ -155,38 +175,51 @@ class GitHub extends AppSource { throw NoReleasesError(); } String? version = targetRelease['tag_name']; - DateTime? releaseDate = targetRelease['published_at'] != null - ? DateTime.parse(targetRelease['published_at']) - : null; + DateTime? releaseDate = getReleaseDateFromRelease(targetRelease); if (version == null) { throw NoVersionError(); } var changeLog = targetRelease['body'].toString(); return APKDetails( version, - getApkUrlsFromUrls(targetRelease['apkUrls'] as List), + targetRelease['apkUrls'] as List>, getAppNames(standardUrl), releaseDate: releaseDate, changeLog: changeLog.isEmpty ? null : changeLog); } else { - rateLimitErrorCheck(res); + if (onHttpErrorCode != null) { + onHttpErrorCode(res); + } throw getObtainiumHttpError(res); } } + @override + Future getLatestAPKDetails( + String standardUrl, + Map additionalSettings, + ) async { + return getLatestAPKDetailsCommon( + 'https://${await getCredentialPrefixIfAny()}api.$host/repos${standardUrl.substring('https://$host'.length)}/releases?per_page=100', + standardUrl, + additionalSettings, onHttpErrorCode: (Response res) { + rateLimitErrorCheck(res); + }); + } + AppNames getAppNames(String standardUrl) { String temp = standardUrl.substring(standardUrl.indexOf('://') + 3); List names = temp.substring(temp.indexOf('/') + 1).split('/'); return AppNames(names[0], names[1]); } - @override - Future> search(String query) async { - Response res = await get(Uri.parse( - 'https://${await getCredentialPrefixIfAny()}api.$host/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100')); + Future> searchCommon( + String query, String requestUrl, String rootProp, + {Function(Response)? onHttpErrorCode}) async { + Response res = await get(Uri.parse(requestUrl)); if (res.statusCode == 200) { Map urlsWithDescriptions = {}; - for (var e in (jsonDecode(res.body)['items'] as List)) { + for (var e in (jsonDecode(res.body)[rootProp] as List)) { urlsWithDescriptions.addAll({ e['html_url'] as String: ((e['archived'] == true ? '[ARCHIVED] ' : '') + @@ -197,11 +230,23 @@ class GitHub extends AppSource { } return urlsWithDescriptions; } else { - rateLimitErrorCheck(res); + if (onHttpErrorCode != null) { + onHttpErrorCode(res); + } throw getObtainiumHttpError(res); } } + @override + Future> search(String query) async { + return searchCommon( + query, + 'https://${await getCredentialPrefixIfAny()}api.$host/search/repositories?q=${Uri.encodeQueryComponent(query)}&per_page=100', + 'items', onHttpErrorCode: (Response res) { + rateLimitErrorCheck(res); + }); + } + rateLimitErrorCheck(Response res) { if (res.headers['x-ratelimit-remaining'] == '0') { throw RateLimitError(