Skip to content
This repository has been archived by the owner on Jun 9, 2023. It is now read-only.

Fix Imgur images loading #202

Merged
merged 4 commits into from
May 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed

### Fixed
- Imgur images (up)loading ([#202](https://github.com/Tunous/Dawn/pull/202))

## [0.9.1] - 2020-05-05
### Fixed
Expand Down
14 changes: 0 additions & 14 deletions app/src/main/java/me/saket/dank/data/ErrorResolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import com.bumptech.glide.load.engine.GlideException
import io.reactivex.exceptions.CompositeException
import io.reactivex.exceptions.UndeliverableException
import me.saket.dank.R
import me.saket.dank.data.exceptions.ImgurApiRequestRateLimitReachedException
import me.saket.dank.data.exceptions.ImgurApiUploadRateLimitReachedException
import okhttp3.internal.http2.ConnectionShutdownException
import okhttp3.internal.http2.StreamResetException
import retrofit2.HttpException
Expand Down Expand Up @@ -57,18 +55,6 @@ constructor() {
R.string.common_reddit_is_down_error_emoji,
R.string.common_reddit_is_down_error_message)

} else if (actualError is ImgurApiRequestRateLimitReachedException) {
ResolvedError.create(
ResolvedError.Type.IMGUR_RATE_LIMIT_REACHED,
R.string.common_imgur_rate_limit_error_emoji,
R.string.common_imgur_request_rate_limit_error_message)

} else if (actualError is ImgurApiUploadRateLimitReachedException) {
ResolvedError.create(
ResolvedError.Type.IMGUR_RATE_LIMIT_REACHED,
R.string.common_imgur_rate_limit_error_emoji,
R.string.common_imgur_upload_rate_limit_error_message)

} else if (actualError is CancellationException || actualError is InterruptedIOException || actualError is InterruptedException) {
ResolvedError.create(
ResolvedError.Type.CANCELATION,
Expand Down
4 changes: 0 additions & 4 deletions app/src/main/java/me/saket/dank/data/ResolvedError.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ public boolean isRedditServerError() {
return type() == Type.REDDIT_IS_DOWN;
}

public boolean isImgurRateLimitError() {
return type() == Type.IMGUR_RATE_LIMIT_REACHED;
}

public static ResolvedError create(Type type, @StringRes int errorEmoji, @StringRes int errorMessage) {
return new AutoValue_ResolvedError(type, errorEmoji, errorMessage);
}
Expand Down

This file was deleted.

This file was deleted.

17 changes: 6 additions & 11 deletions app/src/main/java/me/saket/dank/di/DankApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,15 @@
public interface DankApi {

String HEADER_IMGUR_AUTH = "Authorization: Client-ID 87450e5590435e9";
String HEADER_MASHAPE_KEY = "X-Mashape-Key: VOjpM0pXeAmshuRGE4Hhe6KY9Ouep1YCLx8jsnaivCFNYALpN5";
String HEADER_WHOLESOME_API_AUTH = "Authorization";
String WHOLESOME_API_HOST = "dank-wholesome.herokuapp.com";
String GIPHY_API_KEY = "SFGHZ6SYGn3AzZ07b2tNpENCEDdYTzpB";

// ======== IMGUR ======== //

/**
* Get images in an Imgur album. This is a paid API so we try to minimize its usage. The response
* is wrapped in {@link Response} so that the headers can be extracted for checking Imgur rate-limits.
*/
@CheckResult
@GET("https://imgur-apiv3.p.mashape.com/3/album/{albumId}")
@Headers({ HEADER_IMGUR_AUTH, HEADER_MASHAPE_KEY })
@GET("https://api.imgur.com/3/album/{albumId}")
@Headers({ HEADER_IMGUR_AUTH })
Single<Response<ImgurAlbumResponse>> imgurAlbum(
@Path("albumId") String albumId
);
Expand All @@ -47,16 +42,16 @@ Single<Response<ImgurAlbumResponse>> imgurAlbum(
* Get an image's details from Imgur. This is also a paid API.
*/
@CheckResult
@GET("https://imgur-apiv3.p.mashape.com/3/image/{imageId}")
@Headers({ HEADER_IMGUR_AUTH, HEADER_MASHAPE_KEY })
@GET("https://api.imgur.com/3/image/{imageId}")
@Headers({ HEADER_IMGUR_AUTH })
Single<Response<ImgurImageResponse>> imgurImage(
@Path("imageId") String imageId
);

@CheckResult
@Multipart
@POST("https://imgur-apiv3.p.mashape.com/3/image")
@Headers({ HEADER_IMGUR_AUTH, HEADER_MASHAPE_KEY })
@POST("https://api.imgur.com/3/image")
@Headers({ HEADER_IMGUR_AUTH })
Single<Response<ImgurUploadResponse>> uploadToImgur(
@Part MultipartBody.Part file,
@Query("type") String fileType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,11 @@ private Function<Throwable, Observable<FileUploadProgressEvent<ImgurUploadRespon
String emoji = getResources().getString(resolvedError.errorEmojiRes());
String errorMessage = getResources().getString(resolvedError.errorMessageRes());

if (!resolvedError.isImgurRateLimitError()) {
String tapToRetryText = getResources().getString(R.string.composereply_uploadimage_tap_to_retry);
if (!errorMessage.endsWith(getResources().getString(R.string.composereply_uploadimage_error_message_period))) {
errorMessage += getResources().getString(R.string.composereply_uploadimage_error_message_period);
}
errorMessage += " " + tapToRetryText;
String tapToRetryText = getResources().getString(R.string.composereply_uploadimage_tap_to_retry);
if (!errorMessage.endsWith(getResources().getString(R.string.composereply_uploadimage_error_message_period))) {
errorMessage += getResources().getString(R.string.composereply_uploadimage_error_message_period);
}
errorMessage += " " + tapToRetryText;

errorView.setText(String.format("%s\n\n%s", emoji, errorMessage));
}
Expand Down
149 changes: 6 additions & 143 deletions app/src/main/java/me/saket/dank/ui/media/ImgurRepository.java
Original file line number Diff line number Diff line change
@@ -1,83 +1,53 @@
package me.saket.dank.ui.media;

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;

import androidx.annotation.CheckResult;

import com.jakewharton.rxrelay2.BehaviorRelay;
import com.jakewharton.rxrelay2.Relay;

import java.io.File;
import java.util.TimeZone;

import javax.inject.Inject;
import javax.inject.Singleton;

import hirondelle.date4j.DateTime;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import me.saket.dank.data.FileUploadProgressEvent;
import me.saket.dank.data.exceptions.ImgurApiRequestRateLimitReachedException;
import me.saket.dank.data.exceptions.ImgurApiUploadRateLimitReachedException;
import me.saket.dank.data.exceptions.InvalidImgurAlbumException;
import me.saket.dank.di.DankApi;
import me.saket.dank.urlparser.ImgurAlbumUnresolvedLink;
import me.saket.dank.utils.okhttp.OkHttpRequestBodyWithProgress;
import me.saket.dank.utils.okhttp.OkHttpRequestWriteProgressListener;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.HttpException;
import retrofit2.Response;
import timber.log.Timber;

import static java.lang.Integer.parseInt;

/**
* TODO: Tests.
*/
@Singleton
public class ImgurRepository {

private static final String KEY_REQUEST_LIMIT = "requestsLimit";
private static final String KEY_REMAINING_REQUESTS = "remainingRequests";
private static final String KEY_UPLOAD_LIMIT = "uploadsLimit";
private static final String KEY_REMAINING_UPLOADS = "remainingUploads";
private static final String KEY_RATE_LIMIT_LAST_CHECK = "rateLimitsLastCheck";

/**
* Stop all Imgur requests once we're only left with 10% of our allocated requests.
*/
private static final float LIMIT_THRESHOLD_FACTOR = 0.1f;

private final SharedPreferences sharedPreferences;
private final DankApi dankApi;

@Inject
public ImgurRepository(Application appContext, DankApi dankApi) {
sharedPreferences = appContext.getSharedPreferences(appContext.getPackageName() + "_imgur_ratelimits", Context.MODE_PRIVATE);
public ImgurRepository(Application _appContext, DankApi dankApi) {
this.dankApi = dankApi;
}

/**
* <p>
* TODO: If needed, get the rate limits if they're not cached.
* Remember to handle {@link ImgurApiRequestRateLimitReachedException}.
*
* @throws InvalidImgurAlbumException If an invalid Imgur link was found. Right now this happens only when no images are
* returned by Imgur.
* @throws ImgurApiRequestRateLimitReachedException If Imgur's API limit is reached and no more API requests can be made till the next month.
*/
public Single<ImgurResponse> gallery(ImgurAlbumUnresolvedLink imgurAlbumUnresolvedLink) {
return dankApi.imgurAlbum(imgurAlbumUnresolvedLink.albumId())
.map(throwIfHttpError())
.doOnSuccess(saveImgurApiRateLimits())
.map(response -> response.body())
.map(Response::body)
.onErrorResumeNext(error -> {
// Api returns a 404 when it was a single image and not an album.
if (error instanceof HttpException && ((HttpException) error).code() == 404) {
Expand All @@ -93,29 +63,16 @@ public Single<ImgurResponse> gallery(ImgurAlbumUnresolvedLink imgurAlbumUnresolv
} else {
// Okay, let's check if it was a single image.
return dankApi.imgurImage(imgurAlbumUnresolvedLink.albumId())
.doOnSuccess(saveImgurApiRateLimits())
.map(response -> response.body());
.map(Response::body);
}
})
.doOnSuccess(albumResponse -> {
if (!albumResponse.hasImages()) {
throw new InvalidImgurAlbumException();
}
})
.doOnSubscribe(o -> {
resetRateLimitsIfMonthChanged();

if (isApiRequestLimitReached()) {
throw new ImgurApiRequestRateLimitReachedException();
} else {
Timber.i("Rate limits not reached");
}
});
}

/**
* Remember to handle {@link ImgurApiUploadRateLimitReachedException}.
*/
@CheckResult
public Observable<FileUploadProgressEvent<ImgurUploadResponse>> uploadImage(File imageFile, String mimeType) {
Relay<Float> uploadProgressStream = BehaviorRelay.createDefault(0f);
Expand All @@ -130,22 +87,12 @@ public Observable<FileUploadProgressEvent<ImgurUploadResponse>> uploadImage(File

Observable<FileUploadProgressEvent<ImgurUploadResponse>> uploadStream = dankApi.uploadToImgur(multipartBodyPart, "file")
.map(throwIfHttpError())
.doOnSuccess(saveImgurApiRateLimits())
.map(response -> response.body())
.doOnSubscribe(o -> {
resetRateLimitsIfMonthChanged();

if (isApiUploadLimitReached()) {
throw new ImgurApiUploadRateLimitReachedException();
} else {
Timber.i("Rate limits not reached");
}
})
.map(response -> FileUploadProgressEvent.createUploaded(response))
.map(Response::body)
.map(FileUploadProgressEvent::createUploaded)
.toObservable();

return uploadProgressStream
.map(progress -> FileUploadProgressEvent.<ImgurUploadResponse>createInFlight(progress))
.map(FileUploadProgressEvent::<ImgurUploadResponse>createInFlight)
.mergeWith(uploadStream);
}

Expand All @@ -159,88 +106,4 @@ private <T> Function<Response<T>, Response<T>> throwIfHttpError() {
};
}

private Consumer<Response> saveImgurApiRateLimits() {
return response -> {
Headers responseHeaders = response.headers();
saveApiRequestLimit(parseInt(responseHeaders.get("X-RateLimit-Requests-Limit")));
saveRemainingApiRequests(parseInt(responseHeaders.get("X-RateLimit-Requests-Remaining")));
saveApiUploadLimit(parseInt(responseHeaders.get("X-RateLimit-Uploads-Limit")));
saveRemainingApiUploads(parseInt(responseHeaders.get("X-RateLimit-Uploads-Remaining")));

saveRateLimitsLastCheckedAt(responseHeaders.getDate("Date").getTime());
};
}

private void saveRateLimitsLastCheckedAt(long lastCheckedMillis) {
sharedPreferences.edit().putLong(KEY_RATE_LIMIT_LAST_CHECK, lastCheckedMillis).apply();
}

private long rateLimitsLastCheckedAtMillis(long valueIfEmpty) {
return sharedPreferences.getLong(KEY_RATE_LIMIT_LAST_CHECK, valueIfEmpty);
}

private void resetRateLimitsIfMonthChanged() {
long lastCheckedMillis = rateLimitsLastCheckedAtMillis(-1);
if (lastCheckedMillis == -1) {
return;
}

DateTime lastCheckedDateTime = DateTime.forInstant(lastCheckedMillis, TimeZone.getTimeZone("UTC"));
DateTime nowDateTime = DateTime.now(TimeZone.getTimeZone("UTC"));

Timber.i("Now month: %s, Last checked month: %s", nowDateTime.getMonth(), lastCheckedDateTime.getMonth());

if (nowDateTime.getMonth() > lastCheckedDateTime.getMonth()) {
Timber.i("Months have changed. Resetting Imgur rate limit");

// Months have changed! Reset the limits.
sharedPreferences.edit().clear().apply();
}
}

private void saveRemainingApiRequests(int requestsRemaining) {
sharedPreferences.edit().putInt(KEY_REMAINING_REQUESTS, requestsRemaining).apply();
}

private void saveApiRequestLimit(int requestLimit) {
sharedPreferences.edit().putInt(KEY_REQUEST_LIMIT, requestLimit).apply();
}

private boolean isApiRequestLimitReached() {
if (sharedPreferences.contains(KEY_REMAINING_REQUESTS) && sharedPreferences.contains(KEY_REQUEST_LIMIT)) {
int remainingRequests = sharedPreferences.getInt(KEY_REMAINING_REQUESTS, 0);
int requestLimit = sharedPreferences.getInt(KEY_REQUEST_LIMIT, 0);

Timber.i("remainingRequests: %s", remainingRequests);
Timber.i("requestLimit: %s", requestLimit);

if (remainingRequests < requestLimit * LIMIT_THRESHOLD_FACTOR) {
Timber.w("Imgur api request limit reached :(");
return true;
}
}
return false;
}

private void saveRemainingApiUploads(int uploadsRemaining) {
sharedPreferences.edit().putInt(KEY_REMAINING_UPLOADS, uploadsRemaining).apply();
}

private void saveApiUploadLimit(int uploadLimit) {
sharedPreferences.edit().putInt(KEY_UPLOAD_LIMIT, uploadLimit).apply();
}

private boolean isApiUploadLimitReached() {
if (sharedPreferences.contains(KEY_REMAINING_UPLOADS) && sharedPreferences.contains(KEY_UPLOAD_LIMIT)) {
int remainingUploads = sharedPreferences.getInt(KEY_REMAINING_UPLOADS, 0);
int uploadLimit = sharedPreferences.getInt(KEY_UPLOAD_LIMIT, 0);

if (remainingUploads < uploadLimit * LIMIT_THRESHOLD_FACTOR) {
Timber.i("remainingUploads: %s", remainingUploads);
Timber.i("uploadLimit: %s", uploadLimit);
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,6 @@ private void positionMediaControlsToAvoidOverlappingWithNavBar(WindowInsets inse

private void resolveMediaLinkAndDisplayContent(MediaLink mediaLinkToDisplay) {
mediaHostRepository.resolveActualLinkIfNeeded(mediaLinkToDisplay)
// TODO: Handle Imgur rate limit reached.
.doOnNext(resolvedMediaLink -> this.resolvedMediaLink = resolvedMediaLink)
.map(resolvedMediaLink -> {
// Find all child images under an album.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@
import me.saket.dank.cache.StoreFilePersister;
import me.saket.dank.data.CachedResolvedLinkInfo;
import me.saket.dank.data.FileUploadProgressEvent;
import me.saket.dank.data.exceptions.ImgurApiRequestRateLimitReachedException;
import me.saket.dank.data.exceptions.ImgurApiUploadRateLimitReachedException;
import me.saket.dank.ui.giphy.GiphyGif;
import me.saket.dank.ui.giphy.GiphyRepository;
import me.saket.dank.ui.media.gfycat.GfycatRepository;
Expand Down Expand Up @@ -154,9 +152,6 @@ public void flagLocalUrlParsingAsIncorrect(Link link) {
.subscribe();
}

/**
* Remember to handle {@link ImgurApiRequestRateLimitReachedException}.
*/
public Observable<MediaLink> resolveActualLinkIfNeeded(MediaLink unresolvedLink) {
return incorrectMediaUrlParsingData.get()
.isFlagged(unresolvedLink)
Expand Down Expand Up @@ -215,9 +210,6 @@ private List<ImgurLink> convertImgurImagesToImgurMediaLinks(List<ImgurImage> img
return imgurImageLinks;
}

/**
* Remember to handle {@link ImgurApiUploadRateLimitReachedException}.
*/
@CheckResult
public Observable<FileUploadProgressEvent<ImgurUploadResponse>> uploadImage(File image, String mimeType) {
return imgurRepository.uploadImage(image, mimeType);
Expand Down
Loading