slidge-whatsapp 0.2.2__cp312-cp312-manylinux_2_36_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slidge_whatsapp/__init__.py +17 -0
- slidge_whatsapp/__main__.py +3 -0
- slidge_whatsapp/command.py +143 -0
- slidge_whatsapp/config.py +32 -0
- slidge_whatsapp/contact.py +77 -0
- slidge_whatsapp/event.go +1175 -0
- slidge_whatsapp/gateway.go +181 -0
- slidge_whatsapp/gateway.py +82 -0
- slidge_whatsapp/generated/__init__.py +0 -0
- slidge_whatsapp/generated/_whatsapp.cpython-312-aarch64-linux-gnu.h +606 -0
- slidge_whatsapp/generated/_whatsapp.cpython-312-aarch64-linux-gnu.so +0 -0
- slidge_whatsapp/generated/build.py +395 -0
- slidge_whatsapp/generated/go.py +1632 -0
- slidge_whatsapp/generated/whatsapp.c +6887 -0
- slidge_whatsapp/generated/whatsapp.go +3572 -0
- slidge_whatsapp/generated/whatsapp.py +2911 -0
- slidge_whatsapp/generated/whatsapp_go.h +606 -0
- slidge_whatsapp/go.mod +29 -0
- slidge_whatsapp/go.sum +62 -0
- slidge_whatsapp/group.py +256 -0
- slidge_whatsapp/media/ffmpeg.go +72 -0
- slidge_whatsapp/media/media.go +542 -0
- slidge_whatsapp/media/mupdf.go +47 -0
- slidge_whatsapp/media/stub.go +19 -0
- slidge_whatsapp/session.go +855 -0
- slidge_whatsapp/session.py +745 -0
- slidge_whatsapp-0.2.2.dist-info/LICENSE +661 -0
- slidge_whatsapp-0.2.2.dist-info/METADATA +744 -0
- slidge_whatsapp-0.2.2.dist-info/RECORD +31 -0
- slidge_whatsapp-0.2.2.dist-info/WHEEL +4 -0
- slidge_whatsapp-0.2.2.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
package media
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
// Standard library.
|
|
5
|
+
"bytes"
|
|
6
|
+
"context"
|
|
7
|
+
"fmt"
|
|
8
|
+
"image"
|
|
9
|
+
"image/gif"
|
|
10
|
+
"image/jpeg"
|
|
11
|
+
"image/png"
|
|
12
|
+
"math"
|
|
13
|
+
"os"
|
|
14
|
+
"strconv"
|
|
15
|
+
"strings"
|
|
16
|
+
"time"
|
|
17
|
+
|
|
18
|
+
// Third-party packages.
|
|
19
|
+
"github.com/h2non/filetype"
|
|
20
|
+
"golang.org/x/image/draw"
|
|
21
|
+
"golang.org/x/image/webp"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// MIMEType represents a the media type for a data buffer. In general, values given concrete [MIMEType]
|
|
25
|
+
// identities are meant to be handled as targets for conversion and metadata extraction -- all other
|
|
26
|
+
// formats are handled on a best-case basis.
|
|
27
|
+
type MIMEType string
|
|
28
|
+
|
|
29
|
+
// BaseMediaType returns the media type without any additional parameters.
|
|
30
|
+
func (t MIMEType) BaseMediaType() MIMEType {
|
|
31
|
+
return MIMEType(strings.SplitN(string(t), ";", 2)[0])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const (
|
|
35
|
+
// The fallback MIME type when no concrete MIME type applies.
|
|
36
|
+
TypeUnknown MIMEType = "application/octet-stream"
|
|
37
|
+
|
|
38
|
+
// Audio formats.
|
|
39
|
+
TypeM4A MIMEType = "audio/mp4"
|
|
40
|
+
TypeOgg MIMEType = "audio/ogg"
|
|
41
|
+
|
|
42
|
+
// Video formats.
|
|
43
|
+
TypeMP4 MIMEType = "video/mp4"
|
|
44
|
+
TypeWebM MIMEType = "video/webm"
|
|
45
|
+
|
|
46
|
+
// Image formats.
|
|
47
|
+
TypeJPEG MIMEType = "image/jpeg"
|
|
48
|
+
TypePNG MIMEType = "image/png"
|
|
49
|
+
TypeGIF MIMEType = "image/gif"
|
|
50
|
+
TypeWebP MIMEType = "image/webp"
|
|
51
|
+
|
|
52
|
+
// Document formats.
|
|
53
|
+
TypePDF MIMEType = "application/pdf"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// DetectMIME returns a valid MIME type, as inferred by the data given (usually the first few bytes)
|
|
57
|
+
// or [TypeUnknown] if no valid MIME type could be inferred.
|
|
58
|
+
func DetectMIMEType(data []byte) MIMEType {
|
|
59
|
+
switch t, _ := filetype.Match(data); t.MIME.Value {
|
|
60
|
+
case "audio/m4a":
|
|
61
|
+
return TypeM4A // Correct `audio/m4a` to its valid sub-type.
|
|
62
|
+
case "":
|
|
63
|
+
return TypeUnknown
|
|
64
|
+
default:
|
|
65
|
+
return MIMEType(t.MIME.Value)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// AudioCodec represents the encoding method used for an audio stream.
|
|
70
|
+
type AudioCodec string
|
|
71
|
+
|
|
72
|
+
// VideoCodec represents the encoding method used for a video stream.
|
|
73
|
+
type VideoCodec string
|
|
74
|
+
|
|
75
|
+
const (
|
|
76
|
+
// Audio codecs.
|
|
77
|
+
CodecOpus AudioCodec = "opus"
|
|
78
|
+
CodecAAC AudioCodec = "aac"
|
|
79
|
+
|
|
80
|
+
// Video codecs.
|
|
81
|
+
CodecH264 VideoCodec = "h264"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// Error messages.
|
|
85
|
+
const (
|
|
86
|
+
errInvalidCodec = "media with MIME type %s only support codec %s currently, invalid codec %s chosen"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
// Spec represents the description of a target media file; depending on platform support and media
|
|
90
|
+
// conversion intricacies, source media files can be of any type, even types that aren't represented
|
|
91
|
+
// here. Nevertheless, it is intended that media types and codecs represented here are valid as both
|
|
92
|
+
// input and output formats.
|
|
93
|
+
type Spec struct {
|
|
94
|
+
// Required parameters.
|
|
95
|
+
MIME MIMEType // The MIME type for the target media.
|
|
96
|
+
|
|
97
|
+
// Optional parameters.
|
|
98
|
+
AudioCodec AudioCodec // The codec to use for the audio stream, must be correct for the MIME type given.
|
|
99
|
+
AudioChannels int // The number of channels for the audio stream, 1 for mono, 2 for stereo.
|
|
100
|
+
AudioBitRate int // The bit rate for the audio stream, in kBit/second.
|
|
101
|
+
AudioSampleRate int // The sample-rate frequency for the audio stream, common values are 44100, 48000.
|
|
102
|
+
|
|
103
|
+
VideoCodec VideoCodec // The codec to use for the video stream, must be correct for the MIME type given.
|
|
104
|
+
VideoPixelFormat string // The pixel format used for video stream, typically 'yub420p' for MP4.
|
|
105
|
+
VideoFrameRate int // The frame rate for the video stream, in frames/second.
|
|
106
|
+
VideoWidth int // The width of the video stream, in pixels.
|
|
107
|
+
VideoHeight int // The height of the video stream, in pixels.
|
|
108
|
+
VideoFilter string // A complex filter to apply to the video stream.
|
|
109
|
+
|
|
110
|
+
ImageWidth int // The width of the image, in pixels.
|
|
111
|
+
ImageHeight int // The height of the image, in pixels.
|
|
112
|
+
ImageQuality int // Image quality for lossy image formats, typically a value from 1 to 100.
|
|
113
|
+
ImageFrameRate int // The frame-rate for animated images.
|
|
114
|
+
|
|
115
|
+
DocumentPage int // The number of pages for the document.
|
|
116
|
+
|
|
117
|
+
Duration time.Duration // The duration of the audio or video stream.
|
|
118
|
+
StripMetadata bool // Whether or not to remove any container-level metadata present in the stream.
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// CommandLineArgs returns the current [Spec] as a list of command-line arguments meant for FFMPEG
|
|
122
|
+
// invocations. Where the specification is missing values, default values will be filled where
|
|
123
|
+
// necessary; however, invalid values may have this function return errors.
|
|
124
|
+
func (s Spec) commandLineArgs() ([]string, error) {
|
|
125
|
+
var args []string
|
|
126
|
+
var mime = s.MIME.BaseMediaType()
|
|
127
|
+
|
|
128
|
+
switch mime {
|
|
129
|
+
case TypeOgg, TypeM4A:
|
|
130
|
+
// Audio file format parameters.
|
|
131
|
+
switch mime {
|
|
132
|
+
case TypeOgg:
|
|
133
|
+
if s.AudioCodec != "" && s.AudioCodec != CodecOpus {
|
|
134
|
+
return nil, fmt.Errorf(errInvalidCodec, mime, CodecOpus, s.AudioCodec)
|
|
135
|
+
}
|
|
136
|
+
args = append(args, "-f", "ogg", "-c:a", "libopus")
|
|
137
|
+
case TypeM4A:
|
|
138
|
+
if s.AudioCodec != "" && s.AudioCodec != CodecAAC {
|
|
139
|
+
return nil, fmt.Errorf(errInvalidCodec, mime, CodecAAC, s.AudioCodec)
|
|
140
|
+
}
|
|
141
|
+
args = append(args, "-f", "ipod", "-c:a", "aac")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if s.AudioChannels > 0 {
|
|
145
|
+
args = append(args, "-ac", strconv.Itoa(s.AudioChannels))
|
|
146
|
+
}
|
|
147
|
+
if s.AudioBitRate > 0 {
|
|
148
|
+
args = append(args, "-b:a", strconv.Itoa(s.AudioBitRate)+"k")
|
|
149
|
+
}
|
|
150
|
+
if s.AudioSampleRate > 0 {
|
|
151
|
+
args = append(args, "-ar", strconv.Itoa(s.AudioSampleRate))
|
|
152
|
+
}
|
|
153
|
+
case TypeMP4:
|
|
154
|
+
// Video file format parameters.
|
|
155
|
+
if s.VideoCodec != "" && s.VideoCodec != CodecH264 {
|
|
156
|
+
return nil, fmt.Errorf(errInvalidCodec, mime, CodecH264, s.VideoCodec)
|
|
157
|
+
} else if s.AudioCodec != "" && s.AudioCodec != CodecAAC {
|
|
158
|
+
return nil, fmt.Errorf(errInvalidCodec, mime, CodecAAC, s.AudioCodec)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Set input image frame-rate, e.g. when converting from GIF to MP4.
|
|
162
|
+
if s.ImageFrameRate > 0 {
|
|
163
|
+
args = append(args, "-r", strconv.Itoa(s.ImageFrameRate))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
args = append(args,
|
|
167
|
+
"-f", "mp4", "-c:v", "libx264", "-c:a", "aac",
|
|
168
|
+
"-profile:v", "baseline", // Use Baseline profile for better compatibility.
|
|
169
|
+
"-level", "3.0", // Ensure compatibility with older devices.
|
|
170
|
+
"-movflags", "+faststart", // Use Faststart for quicker rendering.
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if s.VideoPixelFormat != "" {
|
|
174
|
+
args = append(args, "-pix_fmt", s.VideoPixelFormat)
|
|
175
|
+
}
|
|
176
|
+
if s.VideoFilter != "" {
|
|
177
|
+
args = append(args, "-filter:v", s.VideoFilter)
|
|
178
|
+
}
|
|
179
|
+
if s.VideoFrameRate > 0 {
|
|
180
|
+
args = append(args,
|
|
181
|
+
"-r", strconv.Itoa(s.VideoFrameRate),
|
|
182
|
+
"-g", strconv.Itoa(s.VideoFrameRate*2),
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
if s.AudioBitRate > 0 {
|
|
186
|
+
args = append(args, "-b:a", strconv.Itoa(s.AudioBitRate)+"k")
|
|
187
|
+
}
|
|
188
|
+
if s.AudioSampleRate > 0 {
|
|
189
|
+
args = append(args, "-r:a", strconv.Itoa(s.AudioSampleRate))
|
|
190
|
+
}
|
|
191
|
+
case TypeJPEG:
|
|
192
|
+
args = append(args, "-f", "mjpeg", "-qscale:v", "5", "-frames:v", "1")
|
|
193
|
+
|
|
194
|
+
// Scale thumbnail if width/height pixel factors given.
|
|
195
|
+
if s.ImageWidth > 0 || s.ImageHeight > 0 {
|
|
196
|
+
if s.ImageWidth == 0 {
|
|
197
|
+
s.ImageWidth = -1
|
|
198
|
+
} else if s.ImageHeight == 0 {
|
|
199
|
+
s.ImageHeight = -1
|
|
200
|
+
}
|
|
201
|
+
w, h := strconv.FormatInt(int64(s.ImageWidth), 10), strconv.FormatInt(int64(s.ImageHeight), 10)
|
|
202
|
+
args = append(args, "-vf", "scale="+w+":"+h)
|
|
203
|
+
}
|
|
204
|
+
default:
|
|
205
|
+
return nil, fmt.Errorf("cannot process media specification for empty or unknown MIME type")
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if s.StripMetadata {
|
|
209
|
+
args = append(args, "-map_metadata", "-1")
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return args, nil
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Convert processes the given data, assumed to represent a media file, according to the target
|
|
216
|
+
// specification given. For information on how these definitions affect media conversions, see the
|
|
217
|
+
// documentation for the [Spec] type.
|
|
218
|
+
func Convert(ctx context.Context, data []byte, spec *Spec) ([]byte, error) {
|
|
219
|
+
var from, to = DetectMIMEType(data), spec.MIME.BaseMediaType()
|
|
220
|
+
switch from {
|
|
221
|
+
case TypeOgg, TypeM4A:
|
|
222
|
+
switch to {
|
|
223
|
+
case TypeOgg, TypeM4A:
|
|
224
|
+
return convertAudioVideo(ctx, data, spec)
|
|
225
|
+
}
|
|
226
|
+
case TypeMP4, TypeWebM:
|
|
227
|
+
switch to {
|
|
228
|
+
case TypeMP4, TypeJPEG:
|
|
229
|
+
return convertAudioVideo(ctx, data, spec)
|
|
230
|
+
}
|
|
231
|
+
case TypeGIF:
|
|
232
|
+
switch to {
|
|
233
|
+
case TypeMP4:
|
|
234
|
+
return convertAudioVideo(ctx, data, spec)
|
|
235
|
+
case TypeJPEG, TypePNG:
|
|
236
|
+
return convertImage(ctx, data, spec)
|
|
237
|
+
}
|
|
238
|
+
case TypeJPEG, TypePNG, TypeWebP:
|
|
239
|
+
switch to {
|
|
240
|
+
case TypeJPEG, TypePNG:
|
|
241
|
+
return convertImage(ctx, data, spec)
|
|
242
|
+
}
|
|
243
|
+
case TypePDF:
|
|
244
|
+
switch to {
|
|
245
|
+
case TypeJPEG, TypePNG:
|
|
246
|
+
return convertDocument(ctx, data, spec)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return nil, fmt.Errorf("cannot convert file of type '%s' to '%s'", from, to)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ConvertAudioVideo processes the given audio/video data via FFmpeg, for the target specification
|
|
254
|
+
// given. Calls to FFmpeg will be given arguments as per [Spec.commandLineArgs].
|
|
255
|
+
func convertAudioVideo(ctx context.Context, data []byte, spec *Spec) ([]byte, error) {
|
|
256
|
+
args, err := spec.commandLineArgs()
|
|
257
|
+
if err != nil {
|
|
258
|
+
return nil, err
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
in, err := createTempFile(data)
|
|
262
|
+
if err != nil {
|
|
263
|
+
return nil, err
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
defer os.Remove(in)
|
|
267
|
+
|
|
268
|
+
out, err := createTempFile(nil)
|
|
269
|
+
if err != nil {
|
|
270
|
+
return nil, err
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
defer os.Remove(out)
|
|
274
|
+
|
|
275
|
+
if err := ffmpeg(ctx, in, out, args...); err != nil {
|
|
276
|
+
return nil, err
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return os.ReadFile(out)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ConvertImage processes the following image data given via Go-native image processing. Currently,
|
|
283
|
+
// only JPEG and PNG output is allowed, as set in the [Spec.MIME] field.
|
|
284
|
+
func convertImage(_ context.Context, data []byte, spec *Spec) ([]byte, error) {
|
|
285
|
+
img, _, err := image.Decode(bytes.NewReader(data))
|
|
286
|
+
if err != nil {
|
|
287
|
+
return nil, err
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return processImage(img, spec)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ConvertDocument processes the given document, extracting [Spec.PageNumber] as an image of a MIME
|
|
294
|
+
// type corresponding to the given [Spec.MIME]. An error is returned if the data given is not a
|
|
295
|
+
// valid document, or if the page number requested does not exist.
|
|
296
|
+
func convertDocument(ctx context.Context, data []byte, spec *Spec) ([]byte, error) {
|
|
297
|
+
return internalConvertDocument(ctx, data, spec)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ProcessImage handles processing and encoding for the given Go-native image representation.
|
|
301
|
+
func processImage(img image.Image, spec *Spec) ([]byte, error) {
|
|
302
|
+
// Resize image if dimensions given in spec, retaining aspect ratio if either width or height
|
|
303
|
+
// aren't provided.
|
|
304
|
+
if spec.ImageWidth > 0 || spec.ImageHeight > 0 {
|
|
305
|
+
width, height := spec.ImageWidth, spec.ImageHeight
|
|
306
|
+
if width == 0 {
|
|
307
|
+
width = int(float64(img.Bounds().Max.X) / (float64(img.Bounds().Max.Y) / float64(height)))
|
|
308
|
+
} else if height == 0 {
|
|
309
|
+
height = int(float64(img.Bounds().Max.Y) / (float64(img.Bounds().Max.X) / float64(width)))
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
tmp := image.NewRGBA(image.Rect(0, 0, width, height))
|
|
313
|
+
draw.ApproxBiLinear.Scale(tmp, tmp.Rect, img, img.Bounds(), draw.Over, nil)
|
|
314
|
+
img = tmp
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
var err error
|
|
318
|
+
var buf bytes.Buffer
|
|
319
|
+
|
|
320
|
+
// Re-encode image based on target MIME type.
|
|
321
|
+
switch spec.MIME.BaseMediaType() {
|
|
322
|
+
case TypeJPEG:
|
|
323
|
+
o := jpeg.Options{Quality: spec.ImageQuality}
|
|
324
|
+
if o.Quality == 0 {
|
|
325
|
+
o.Quality = jpeg.DefaultQuality
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
err = jpeg.Encode(&buf, img, nil)
|
|
329
|
+
case TypePNG:
|
|
330
|
+
err = png.Encode(&buf, img)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if err != nil {
|
|
334
|
+
return nil, err
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return buf.Bytes(), nil
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// GetSpec returns a media specification corresponding to the data given. The [Spec] value returned
|
|
341
|
+
// will only have its fields partially populated, as not all values can be derived accurately.
|
|
342
|
+
func GetSpec(ctx context.Context, data []byte) (*Spec, error) {
|
|
343
|
+
switch DetectMIMEType(data) {
|
|
344
|
+
case TypeJPEG, TypePNG, TypeGIF, TypeWebP:
|
|
345
|
+
return getImageSpec(ctx, data)
|
|
346
|
+
case TypePDF:
|
|
347
|
+
return getDocumentSpec(ctx, data)
|
|
348
|
+
default:
|
|
349
|
+
// Assume file is some form of audio or video file, and attempt best-effort extraction of spec.
|
|
350
|
+
return getAudioVideoSpec(ctx, data)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// GetAudioVideoSpec attempts to fetch as much metadata as possible from the given data buffer, which
|
|
355
|
+
// is assumed to be some form of audio of video file, via FFmpeg.
|
|
356
|
+
func getAudioVideoSpec(ctx context.Context, data []byte) (*Spec, error) {
|
|
357
|
+
in, err := createTempFile(data)
|
|
358
|
+
if err != nil {
|
|
359
|
+
return nil, err
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
defer os.Remove(in)
|
|
363
|
+
var result Spec
|
|
364
|
+
|
|
365
|
+
out, err := ffprobe(ctx, in,
|
|
366
|
+
"-show_entries", "stream=codec_name,width,height,sample_rate,duration",
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if s, ok := out["streams"].([]any); ok {
|
|
370
|
+
if len(s) == 0 {
|
|
371
|
+
return nil, fmt.Errorf("no valid audio/video streams found in data")
|
|
372
|
+
} else if r, ok := s[0].(map[string]any); ok {
|
|
373
|
+
if v, ok := r["duration"].(string); ok {
|
|
374
|
+
if v, err := strconv.ParseFloat(v, 64); err == nil {
|
|
375
|
+
result.Duration = time.Duration(v * float64(time.Second))
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if v, ok := r["width"].(string); ok {
|
|
379
|
+
if v, err := strconv.Atoi(v); err == nil {
|
|
380
|
+
result.VideoWidth = v
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if v, ok := r["height"].(string); ok {
|
|
384
|
+
if v, err := strconv.Atoi(v); err == nil {
|
|
385
|
+
result.VideoHeight = v
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if v, ok := r["sample_rate"].(string); ok {
|
|
389
|
+
if v, err := strconv.Atoi(v); err == nil {
|
|
390
|
+
result.AudioSampleRate = v
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if v, ok := r["codec_name"].(string); ok {
|
|
394
|
+
if result.VideoWidth > 0 || result.VideoHeight > 0 {
|
|
395
|
+
result.VideoCodec = VideoCodec(v)
|
|
396
|
+
} else {
|
|
397
|
+
result.AudioCodec = AudioCodec(v)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return &result, nil
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// GetImageSpec fetches as much metadata as possible from the given data buffer, which is assumed to
|
|
407
|
+
// be a valid image (e.g. a JPEG, PNG, GIF) file.
|
|
408
|
+
func getImageSpec(_ context.Context, data []byte) (*Spec, error) {
|
|
409
|
+
var err error
|
|
410
|
+
var config image.Config
|
|
411
|
+
var buf = bytes.NewReader(data)
|
|
412
|
+
|
|
413
|
+
switch DetectMIMEType(data) {
|
|
414
|
+
case TypeGIF:
|
|
415
|
+
dec, err := gif.DecodeAll(buf)
|
|
416
|
+
if err != nil {
|
|
417
|
+
return nil, err
|
|
418
|
+
}
|
|
419
|
+
var spec Spec
|
|
420
|
+
if len(dec.Image) > 1 {
|
|
421
|
+
var t float64
|
|
422
|
+
for d := range dec.Delay {
|
|
423
|
+
t += float64(d) / 100
|
|
424
|
+
}
|
|
425
|
+
if t > 0 {
|
|
426
|
+
spec.ImageFrameRate = int(float64(len(dec.Image)) / t)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
s := dec.Image[0].Bounds().Max
|
|
430
|
+
spec.ImageWidth, spec.ImageHeight = s.X, s.Y
|
|
431
|
+
return &spec, nil
|
|
432
|
+
case TypeJPEG:
|
|
433
|
+
config, err = jpeg.DecodeConfig(buf)
|
|
434
|
+
case TypePNG:
|
|
435
|
+
config, err = png.DecodeConfig(buf)
|
|
436
|
+
case TypeWebP:
|
|
437
|
+
config, err = webp.DecodeConfig(buf)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if err != nil {
|
|
441
|
+
return nil, err
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return &Spec{
|
|
445
|
+
ImageWidth: config.Width,
|
|
446
|
+
ImageHeight: config.Height,
|
|
447
|
+
}, nil
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// GetDocumentSpec fetches as much metadata as possible from the given data buffer, which is assumed
|
|
451
|
+
// to be a valid PDF file.
|
|
452
|
+
func getDocumentSpec(ctx context.Context, data []byte) (*Spec, error) {
|
|
453
|
+
return internalGetDocumentSpec(ctx, data)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// GetWaveform returns a list of samples, scaled from 0 to 100, representing linear loudness values.
|
|
457
|
+
//
|
|
458
|
+
// An error will be returned if the [Spec] given has no sample-rate or duration corresponding to the
|
|
459
|
+
// data given, as both these values are necessary for deriving the number of samples.
|
|
460
|
+
//
|
|
461
|
+
// The number of samples returned will be equal to the given maximum number provided, and will be
|
|
462
|
+
// padded with 0 values if necessary.
|
|
463
|
+
func GetWaveform(ctx context.Context, data []byte, spec *Spec, maxSamples int) ([]byte, error) {
|
|
464
|
+
if spec.AudioSampleRate == 0 || spec.Duration == 0 {
|
|
465
|
+
return nil, fmt.Errorf("no sample-rate or duration for media given")
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
in, err := createTempFile(data)
|
|
469
|
+
if err != nil {
|
|
470
|
+
return nil, err
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
defer os.Remove(in)
|
|
474
|
+
|
|
475
|
+
// Determine number of waveform to take based on duration and sample-rate of original file.
|
|
476
|
+
numSamples := strconv.Itoa(int(float64(spec.AudioSampleRate)*spec.Duration.Seconds()) / maxSamples)
|
|
477
|
+
out, err := ffprobe(ctx,
|
|
478
|
+
"amovie="+in+",asetnsamples="+numSamples+",astats=metadata=1:reset=1",
|
|
479
|
+
"-f", "lavfi",
|
|
480
|
+
"-show_entries", "frame_tags=lavfi.astats.Overall.Peak_level",
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
// Get waveform with defined maximum number of samples, and scale these from a range of 0 to 100.
|
|
484
|
+
var samples = make([]byte, 0, maxSamples)
|
|
485
|
+
if f, ok := out["frames"].([]any); ok {
|
|
486
|
+
if len(f) == 0 {
|
|
487
|
+
return nil, fmt.Errorf("no audio frames found in media")
|
|
488
|
+
}
|
|
489
|
+
for i := range f {
|
|
490
|
+
if r, ok := f[i].(map[string]any); ok {
|
|
491
|
+
if t, ok := r["tags"].(map[string]any); ok {
|
|
492
|
+
if v, ok := t["lavfi.astats.Overall.Peak_level"].(string); ok {
|
|
493
|
+
db, err := strconv.ParseFloat(v, 64)
|
|
494
|
+
if err == nil {
|
|
495
|
+
samples = append(samples, byte(math.Pow(10, (db/50))*100))
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return samples, nil
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
var (
|
|
507
|
+
// The default path for storing temporary files.
|
|
508
|
+
tempDir = os.TempDir()
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
// SetTempDirectory sets the global temporary directory used internally by media conversion commands.
|
|
512
|
+
func SetTempDirectory(path string) error {
|
|
513
|
+
if _, err := os.Stat(path); err != nil {
|
|
514
|
+
return err
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
tempDir = path
|
|
518
|
+
return nil
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// CreateTempFile creates a temporary file in the pre-defined temporary directory (or the default,
|
|
522
|
+
// system-wide temporary directory, if no override value was set) and returns the absolute path for
|
|
523
|
+
// the file, or an error if none could be created.
|
|
524
|
+
func createTempFile(data []byte) (string, error) {
|
|
525
|
+
f, err := os.CreateTemp(tempDir, "media-*")
|
|
526
|
+
if err != nil {
|
|
527
|
+
return "", fmt.Errorf("failed creating temporary file: %w", err)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
defer f.Close()
|
|
531
|
+
if len(data) > 0 {
|
|
532
|
+
if n, err := f.Write(data); err != nil {
|
|
533
|
+
os.Remove(f.Name())
|
|
534
|
+
return "", fmt.Errorf("failed writing to temporary file: %w", err)
|
|
535
|
+
} else if n < len(data) {
|
|
536
|
+
os.Remove(f.Name())
|
|
537
|
+
return "", fmt.Errorf("failed writing to temporary file: incomplete write, want %d, write %d bytes", len(data), n)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return f.Name(), nil
|
|
542
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//go:build mupdf
|
|
2
|
+
|
|
3
|
+
package media
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
// Standard library.
|
|
7
|
+
"context"
|
|
8
|
+
"fmt"
|
|
9
|
+
|
|
10
|
+
// Third-party packages.
|
|
11
|
+
"github.com/gen2brain/go-fitz"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// InternalConvertDocument converts the given data buffer, which is assumed to be a valid PDF document,
|
|
15
|
+
// into the target spec with MuPDF.
|
|
16
|
+
func internalConvertDocument(_ context.Context, data []byte, spec *Spec) ([]byte, error) {
|
|
17
|
+
doc, err := fitz.NewFromMemory(data)
|
|
18
|
+
if err != nil {
|
|
19
|
+
return nil, fmt.Errorf("failed reading document: %s", err)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
defer doc.Close()
|
|
23
|
+
|
|
24
|
+
var buf []byte
|
|
25
|
+
if n := doc.NumPage(); n <= spec.DocumentPage {
|
|
26
|
+
return nil, fmt.Errorf("cannot read page %d in document with %d pages", spec.DocumentPage+1, n+1)
|
|
27
|
+
} else if img, err := doc.Image(spec.DocumentPage); err != nil {
|
|
28
|
+
if buf, err = processImage(img, spec); err != nil {
|
|
29
|
+
return nil, err
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return buf, nil
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// InternalGetDocumentSpec fetches as much metadata as possible from the given data buffer with MuPDF.
|
|
37
|
+
func internalGetDocumentSpec(_ context.Context, data []byte) (*Spec, error) {
|
|
38
|
+
doc, err := fitz.NewFromMemory(data)
|
|
39
|
+
if err != nil {
|
|
40
|
+
return nil, fmt.Errorf("failed to read document: %s", err)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
defer doc.Close()
|
|
44
|
+
return &Spec{
|
|
45
|
+
DocumentPage: doc.NumPage(),
|
|
46
|
+
}, nil
|
|
47
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//go:build !mupdf
|
|
2
|
+
|
|
3
|
+
package media
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
// Standard library.
|
|
7
|
+
"context"
|
|
8
|
+
"errors"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// InternalGetDocumentSpec is a stub implementation, as called by [convertDocument].
|
|
12
|
+
func internalConvertDocument(_ context.Context, _ []byte, _ *Spec) ([]byte, error) {
|
|
13
|
+
return nil, errors.New("document support not enabled in this build")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// InternalGetDocumentSpec is a stub implementation, as called by [getDocumentSpec].
|
|
17
|
+
func internalGetDocumentSpec(_ context.Context, _ []byte) (*Spec, error) {
|
|
18
|
+
return nil, errors.New("document support not enabled in this build")
|
|
19
|
+
}
|