Skip to content

Commit f640fff

Browse files
Manually use ffprobe to check file informations
Also add a sanity check inside the docker image
1 parent 2f03ec8 commit f640fff

File tree

11 files changed

+149
-136
lines changed

11 files changed

+149
-136
lines changed

.dockerignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
.gradle/
44
.idea/
55
build/
6-
src/test/
76
src/main/resources/images/
87
.gitattributes
98
.gitignore

Dockerfile

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
FROM eclipse-temurin:25-alpine AS builder
22

33
WORKDIR /app
4+
5+
# bump: ffmpeg /static-ffmpeg:([\d.]+)/ docker:mwader/static-ffmpeg|~7.0
6+
COPY --from=mwader/static-ffmpeg:7.0.2 /ff* /usr/bin/
7+
48
COPY . .
5-
RUN --mount=type=cache,target=/root/.gradle ./gradlew installDist
9+
RUN --mount=type=cache,target=/root/.gradle ./gradlew check installDist --no-daemon
610

711
# bump: alpine /FROM alpine:([\d.]+)/ docker:alpine|^3
812
# bump: alpine link "Release notes" https://alpinelinux.org/posts/Alpine-$LATEST-released.html
9-
FROM alpine:3.22.1 AS bot
13+
FROM alpine:3.22.1
1014

11-
# bump: ffmpeg /static-ffmpeg:([\d.]+)/ docker:mwader/static-ffmpeg|~7.0
12-
COPY --from=mwader/static-ffmpeg:7.0.2 /ffmpeg /usr/bin/
15+
COPY --from=builder /usr/bin/ff* /usr/bin/
1316
COPY --from=builder /app/build/install/Stickerify/ .
1417

1518
ENV CONCURRENT_PROCESSES=5
16-
ENV FFMPEG_PATH=/usr/bin/ffmpeg
1719
CMD ["./bin/Stickerify"]

build.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ repositories {
1515
dependencies {
1616
compileOnly(libs.jspecify)
1717
implementation(libs.gson)
18-
implementation(libs.jave)
1918
implementation(libs.logback.classic)
2019
implementation(libs.telegram.bot.api)
2120
implementation(libs.tika)

gradle/libs.versions.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
[libraries]
22
gson = "com.google.code.gson:gson:2.13.2"
33
hamcrest = "org.hamcrest:hamcrest:3.0"
4-
jave = "ws.schild:jave-core:3.5.0"
54
jspecify = "org.jspecify:jspecify:1.0.0"
65
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.13.4"
76
junit-platform = "org.junit.platform:junit-platform-launcher:1.13.4"

src/main/java/com/github/stickerifier/stickerify/bot/Stickerify.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import static java.util.concurrent.Executors.newThreadPerTaskExecutor;
1111

1212
import com.github.stickerifier.stickerify.exception.BaseException;
13-
import com.github.stickerifier.stickerify.exception.CorruptedVideoException;
13+
import com.github.stickerifier.stickerify.exception.CorruptedFileException;
1414
import com.github.stickerifier.stickerify.exception.FileOperationException;
1515
import com.github.stickerifier.stickerify.exception.MediaException;
1616
import com.github.stickerifier.stickerify.exception.TelegramApiException;
@@ -175,7 +175,7 @@ private void processFailure(TelegramRequest request, BaseException e, String fil
175175
processTelegramFailure(request.getDescription(), telegramException, false);
176176
}
177177

178-
if (e instanceof CorruptedVideoException) {
178+
if (e instanceof CorruptedFileException) {
179179
LOGGER.atInfo().log("Unable to reply to the {}: the file is corrupted", request.getDescription());
180180
answerText(CORRUPTED, request);
181181
} else {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.github.stickerifier.stickerify.exception;
2+
3+
public class CorruptedFileException extends MediaException {
4+
public CorruptedFileException(String message, Throwable cause) {
5+
super(message, cause);
6+
}
7+
}

src/main/java/com/github/stickerifier/stickerify/exception/CorruptedVideoException.java

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/main/java/com/github/stickerifier/stickerify/media/MediaHelper.java

Lines changed: 112 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@
1212
import static com.github.stickerifier.stickerify.media.MediaConstraints.VP9_CODEC;
1313
import static java.nio.charset.StandardCharsets.UTF_8;
1414

15-
import com.github.stickerifier.stickerify.exception.CorruptedVideoException;
15+
import com.github.stickerifier.stickerify.exception.CorruptedFileException;
1616
import com.github.stickerifier.stickerify.exception.FileOperationException;
1717
import com.github.stickerifier.stickerify.exception.MediaException;
1818
import com.github.stickerifier.stickerify.exception.ProcessException;
1919
import com.github.stickerifier.stickerify.process.OsConstants;
20-
import com.github.stickerifier.stickerify.process.PathLocator;
2120
import com.github.stickerifier.stickerify.process.ProcessHelper;
2221
import com.google.gson.Gson;
2322
import com.google.gson.JsonSyntaxException;
@@ -26,14 +25,12 @@
2625
import org.jspecify.annotations.Nullable;
2726
import org.slf4j.Logger;
2827
import org.slf4j.LoggerFactory;
29-
import ws.schild.jave.EncoderException;
30-
import ws.schild.jave.MultimediaObject;
31-
import ws.schild.jave.info.MultimediaInfo;
3228

3329
import java.io.File;
3430
import java.io.FileInputStream;
3531
import java.io.IOException;
3632
import java.nio.file.Files;
33+
import java.util.List;
3734
import java.util.Set;
3835
import java.util.zip.GZIPInputStream;
3936

@@ -128,34 +125,105 @@ private static boolean isSupportedVideo(String mimeType) {
128125
* @param file the file to check
129126
* @return {@code true} if the file is compliant
130127
* @throws FileOperationException if an error occurred retrieving the size of the file
128+
* @throws InterruptedException if the current thread is interrupted while retrieving file info
131129
*/
132-
private static boolean isVideoCompliant(File file) throws FileOperationException, CorruptedVideoException {
130+
private static boolean isVideoCompliant(File file) throws FileOperationException, CorruptedFileException, InterruptedException {
133131
var mediaInfo = retrieveMultimediaInfo(file);
134-
var videoInfo = mediaInfo.getVideo();
135-
var videoSize = videoInfo.getSize();
136-
137-
return isSizeCompliant(videoSize.getWidth(), videoSize.getHeight())
138-
&& videoInfo.getFrameRate() <= MAX_VIDEO_FRAMES
139-
&& videoInfo.getDecoder().startsWith(VP9_CODEC)
140-
&& mediaInfo.getDuration() > 0L
141-
&& mediaInfo.getDuration() <= MAX_VIDEO_DURATION_MILLIS
142-
&& mediaInfo.getAudio() == null
143-
&& MATROSKA_FORMAT.equals(mediaInfo.getFormat())
144-
&& isFileSizeLowerThan(file, MAX_VIDEO_FILE_SIZE);
132+
133+
var formatInfo = mediaInfo.format();
134+
if (formatInfo == null) {
135+
return false; // not a video, maybe throw
136+
}
137+
138+
var duration = formatInfo.durationAsMillis();
139+
if (duration == null) {
140+
return false; // not a video, maybe throw
141+
}
142+
143+
var videoInfo = mediaInfo.video();
144+
if (videoInfo == null) {
145+
return false; // not a video, maybe throw
146+
}
147+
148+
var size = formatInfo.sizeAsLong();
149+
var frameRate = videoInfo.frameRateAsFloat();
150+
151+
return isSizeCompliant(videoInfo.width(), videoInfo.height())
152+
&& frameRate <= MAX_VIDEO_FRAMES
153+
&& VP9_CODEC.equals(videoInfo.codec())
154+
&& duration <= MAX_VIDEO_DURATION_MILLIS
155+
&& mediaInfo.audio() == null
156+
&& formatInfo.format().startsWith(MATROSKA_FORMAT)
157+
&& size <= MAX_VIDEO_FILE_SIZE;
145158
}
146159

147160
/**
148161
* Convenience method to retrieve multimedia information of a file.
149162
*
150163
* @param file the video to check
151164
* @return passed-in video's multimedia information
152-
* @throws CorruptedVideoException if an error occurred retrieving video information
165+
* @throws CorruptedFileException if an error occurred retrieving file information
166+
* @throws InterruptedException if the current thread is interrupted while retrieving file info
153167
*/
154-
private static MultimediaInfo retrieveMultimediaInfo(File file) throws CorruptedVideoException {
168+
static MultimediaInfo retrieveMultimediaInfo(File file) throws CorruptedFileException, InterruptedException {
169+
var command = new String[] {
170+
"ffprobe",
171+
"-hide_banner",
172+
"-v", "quiet",
173+
"-print_format", "json",
174+
"-show_format",
175+
"-show_streams",
176+
file.getAbsolutePath()
177+
};
178+
155179
try {
156-
return new MultimediaObject(file, PathLocator.INSTANCE).getInfo();
157-
} catch (EncoderException e) {
158-
throw new CorruptedVideoException("The video could not be processed successfully", e);
180+
var lines = ProcessHelper.executeCommand(command);
181+
var body = String.join("\n", lines);
182+
183+
return GSON.fromJson(body, MultimediaInfo.class);
184+
} catch (ProcessException | JsonSyntaxException e) {
185+
throw new CorruptedFileException("The file could not be processed successfully", e);
186+
}
187+
}
188+
189+
record MultimediaInfo(List<StreamInfo> streams, @Nullable FormatInfo format) {
190+
@Nullable StreamInfo audio() {
191+
return streams.stream()
192+
.filter(s -> StreamInfo.TYPE_AUDIO.equals(s.type))
193+
.findFirst()
194+
.orElse(null);
195+
}
196+
197+
@Nullable StreamInfo video() {
198+
return streams.stream()
199+
.filter(s -> StreamInfo.TYPE_VIDEO.equals(s.type))
200+
.findFirst()
201+
.orElse(null);
202+
}
203+
}
204+
record StreamInfo(@SerializedName("codec_name") String codec, @SerializedName("codec_type") String type, int width, int height, @SerializedName("avg_frame_rate") String frameRateFraction) {
205+
private static final String TYPE_AUDIO = "audio";
206+
private static final String TYPE_VIDEO = "video";
207+
208+
float frameRateAsFloat() {
209+
if (frameRateFraction.contains("/")) {
210+
var ratio = frameRateFraction.split("/");
211+
return Float.parseFloat(ratio[0]) / Float.parseFloat(ratio[1]);
212+
} else {
213+
return Float.parseFloat(frameRateFraction);
214+
}
215+
}
216+
}
217+
record FormatInfo(@SerializedName("format_name") String format, @SerializedName("duration") @Nullable String duration, String size) {
218+
@Nullable Long durationAsMillis() {
219+
if (duration == null) {
220+
return null;
221+
}
222+
return (long) (Float.parseFloat(duration) * 1000);
223+
}
224+
225+
long sizeAsLong() {
226+
return Long.parseLong(size);
159227
}
160228
}
161229

@@ -183,7 +251,11 @@ private static boolean isAnimatedStickerCompliant(File file, String mimeType) th
183251

184252
boolean isAnimationCompliant = isAnimationCompliant(sticker);
185253
if (isAnimationCompliant) {
186-
return isFileSizeLowerThan(file, MAX_ANIMATION_FILE_SIZE);
254+
try {
255+
return Files.size(file.toPath()) <= MAX_ANIMATION_FILE_SIZE;
256+
} catch (IOException e) {
257+
throw new FileOperationException(e);
258+
}
187259
}
188260

189261
LOGGER.atWarn().log("The {} doesn't meet Telegram's requirements", sticker);
@@ -226,38 +298,33 @@ private static boolean isAnimationCompliant(@Nullable AnimationDetails animation
226298
&& animation.height() == MAX_SIDE_LENGTH;
227299
}
228300

229-
/**
230-
* Checks that passed-in file's size does not exceed the specified threshold.
231-
*
232-
* @param file the file to check
233-
* @param threshold max allowed file size
234-
* @return {@code true} if file's size is compliant
235-
* @throws FileOperationException if an error occurred retrieving the size of the file
236-
*/
237-
private static boolean isFileSizeLowerThan(File file, long threshold) throws FileOperationException {
238-
try {
239-
return Files.size(file.toPath()) <= threshold;
240-
} catch (IOException e) {
241-
throw new FileOperationException(e);
242-
}
243-
}
244-
245301
/**
246302
* Checks if passed-in image is already compliant with Telegram's requisites.
247303
*
248304
* @param image the image to check
249305
* @param mimeType the MIME type of the file
250306
* @return {@code true} if the file is compliant
307+
* @throws CorruptedFileException if an error occurred retrieving image information
308+
* @throws InterruptedException if the current thread is interrupted while retrieving file info
251309
*/
252-
private static boolean isImageCompliant(File image, String mimeType) throws CorruptedVideoException, FileOperationException {
253-
var videoSize = retrieveMultimediaInfo(image).getVideo().getSize();
254-
if (videoSize == null) {
255-
throw new FileOperationException("Couldn't read image dimensions");
310+
private static boolean isImageCompliant(File image, String mimeType) throws CorruptedFileException, InterruptedException {
311+
var mediaInfo = retrieveMultimediaInfo(image);
312+
313+
var formatInfo = mediaInfo.format();
314+
if (formatInfo == null) {
315+
return false; // not an image, maybe throw
256316
}
257317

318+
var videoInfo = mediaInfo.video();
319+
if (videoInfo == null) {
320+
return false; // not an image, maybe throw
321+
}
322+
323+
var size = formatInfo.sizeAsLong();
324+
258325
return ("image/png".equals(mimeType) || "image/webp".equals(mimeType))
259-
&& isSizeCompliant(videoSize.getWidth(), videoSize.getHeight())
260-
&& isFileSizeLowerThan(image, MAX_IMAGE_FILE_SIZE);
326+
&& isSizeCompliant(videoInfo.width(), videoInfo.height())
327+
&& size <= MAX_IMAGE_FILE_SIZE;
261328
}
262329

263330
/**

src/main/java/com/github/stickerifier/stickerify/process/OsConstants.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
public final class OsConstants {
44
private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase().contains("windows");
55

6-
public static final String FIND_EXECUTABLE = IS_WINDOWS ? "where" : "which";
76
public static final String NULL_FILE = IS_WINDOWS ? "NUL" : "/dev/null";
87

98
private OsConstants() {

src/main/java/com/github/stickerifier/stickerify/process/PathLocator.java

Lines changed: 0 additions & 49 deletions
This file was deleted.

0 commit comments

Comments
 (0)