Git Repository
Mini Video Library
Mini Video Library scannt lokale Medienordner, verwaltet Metadaten und Vorschaubilder und streamt kompatible Videos direkt per HTTP an den Browser.
HTTPS
https://zanvex.de/git/mini-video-library.gitinternal/ffmpeginstall/install.go
Zum Verzeichnispackage ffmpeginstall
import (
"archive/zip"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
const DownloadURL = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
type Result struct{ FFmpeg, FFprobe string }
func Install(ctx context.Context, destination string) (Result, error) {
if runtime.GOOS != "windows" {
return Result{}, fmt.Errorf("automatic installation is currently only available on Windows")
}
if err := os.MkdirAll(destination, 0755); err != nil {
return Result{}, err
}
tmp, err := os.CreateTemp(destination, "ffmpeg-*.zip")
if err != nil {
return Result{}, err
}
archive := tmp.Name()
defer os.Remove(archive)
defer tmp.Close()
client := &http.Client{Timeout: 20 * time.Minute}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, DownloadURL, nil)
if err != nil {
return Result{}, err
}
resp, err := client.Do(req)
if err != nil {
return Result{}, fmt.Errorf("download FFmpeg: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return Result{}, fmt.Errorf("download FFmpeg: HTTP %s", resp.Status)
}
hash := sha256.New()
if _, err = io.Copy(io.MultiWriter(tmp, hash), io.LimitReader(resp.Body, 300<<20)); err != nil {
return Result{}, err
}
if err = tmp.Close(); err != nil {
return Result{}, err
}
if err = verifyChecksum(ctx, client, hex.EncodeToString(hash.Sum(nil))); err != nil {
return Result{}, err
}
return extractExecutables(archive, destination)
}
func verifyChecksum(ctx context.Context, client *http.Client, actual string) error {
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, DownloadURL+".sha256", nil)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("download checksum: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download checksum: HTTP %s", resp.Status)
}
b, err := io.ReadAll(io.LimitReader(resp.Body, 1024))
if err != nil {
return err
}
expected := strings.Fields(string(b))
if len(expected) == 0 || !strings.EqualFold(expected[0], actual) {
return fmt.Errorf("FFmpeg archive checksum does not match")
}
return nil
}
func extractExecutables(archive, destination string) (Result, error) {
z, err := zip.OpenReader(archive)
if err != nil {
return Result{}, fmt.Errorf("open FFmpeg archive: %w", err)
}
defer z.Close()
wanted := map[string]string{"ffmpeg.exe": "", "ffprobe.exe": ""}
for _, f := range z.File {
name := strings.ToLower(filepath.Base(filepath.FromSlash(f.Name)))
if _, ok := wanted[name]; !ok || !strings.Contains(strings.ToLower(f.Name), "/bin/") {
continue
}
r, err := f.Open()
if err != nil {
return Result{}, err
}
target, temp := filepath.Join(destination, name), filepath.Join(destination, name+".new")
out, err := os.OpenFile(temp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755)
if err == nil {
_, err = io.Copy(out, io.LimitReader(r, 250<<20))
if closeErr := out.Close(); err == nil {
err = closeErr
}
}
r.Close()
if err != nil {
os.Remove(temp)
return Result{}, err
}
os.Remove(target)
if err = os.Rename(temp, target); err != nil {
return Result{}, err
}
abs, _ := filepath.Abs(target)
wanted[name] = abs
}
if wanted["ffmpeg.exe"] == "" || wanted["ffprobe.exe"] == "" {
return Result{}, fmt.Errorf("archive does not contain ffmpeg.exe and ffprobe.exe")
}
return Result{FFmpeg: wanted["ffmpeg.exe"], FFprobe: wanted["ffprobe.exe"]}, nil
}