mirror of
https://github.com/photoprism/photoprism.git
synced 2025-09-26 21:01:58 +08:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# Ubuntu 25.04 (Plucky Puffin)
|
||||
FROM photoprism/develop:250524-plucky
|
||||
FROM photoprism/develop:250608-plucky
|
||||
|
||||
## Alternative Environments:
|
||||
# FROM photoprism/develop:armv7 # ARMv7 (32bit)
|
||||
|
4
Makefile
4
Makefile
@@ -74,6 +74,7 @@ test: test-js test-go
|
||||
test-go: reset-sqlite run-test-go
|
||||
test-pkg: reset-sqlite run-test-pkg
|
||||
test-api: reset-sqlite run-test-api
|
||||
test-video: reset-sqlite run-test-video
|
||||
test-entity: reset-sqlite run-test-entity
|
||||
test-commands: reset-sqlite run-test-commands
|
||||
test-photoprism: reset-sqlite run-test-photoprism
|
||||
@@ -376,6 +377,9 @@ run-test-pkg:
|
||||
run-test-api:
|
||||
$(info Running all API tests...)
|
||||
$(GOTEST) -parallel 2 -count 1 -cpu 2 -tags="slow,develop" -timeout 20m ./internal/api/...
|
||||
run-test-video:
|
||||
$(info Running all video tests...)
|
||||
$(GOTEST) -parallel 2 -count 1 -cpu 2 -tags="slow,develop" -timeout 20m ./internal/ffmpeg/... ./internal/photoprism/dl/... ./pkg/media/...
|
||||
run-test-entity:
|
||||
$(info Running all Entity tests...)
|
||||
$(GOTEST) -parallel 2 -count 1 -cpu 2 -tags="slow,develop" -timeout 20m ./internal/entity/...
|
||||
|
BIN
assets/examples/bear.m2ts
Normal file
BIN
assets/examples/bear.m2ts
Normal file
Binary file not shown.
BIN
assets/examples/m2ts.mp4
Normal file
BIN
assets/examples/m2ts.mp4
Normal file
Binary file not shown.
@@ -38,6 +38,7 @@ export const FormatWebmAv1 = "webm_av1";
|
||||
export const FormatMkvAv1 = "mkv_av1";
|
||||
export const FormatTheora = "ogg";
|
||||
export const FormatWebp = "webp";
|
||||
export const FormatM2TS = "m2t";
|
||||
|
||||
// Image file formats:
|
||||
export const FormatJpeg = "jpg";
|
||||
|
@@ -423,8 +423,9 @@ export default class $util {
|
||||
return "Matroska Multimedia Container";
|
||||
case "mts":
|
||||
return "Advanced Video Coding High Definition (AVCHD)";
|
||||
case "m2t":
|
||||
case "m2ts":
|
||||
return "Blu-ray MPEG-2 Transport Stream";
|
||||
return "MPEG-2 Transport Stream (M2TS)";
|
||||
case "webp":
|
||||
return "Google WebP";
|
||||
case media.FormatWebm:
|
||||
@@ -524,6 +525,8 @@ export default class $util {
|
||||
case media.CodecVp09:
|
||||
case media.FormatVp9:
|
||||
return "VP9";
|
||||
case media.FormatM2TS:
|
||||
return "M2TS";
|
||||
case "extended webp":
|
||||
case media.FormatWebp:
|
||||
return "WebP";
|
||||
@@ -578,6 +581,9 @@ export default class $util {
|
||||
return "Extended WebP";
|
||||
case "webm":
|
||||
return "Google WebM";
|
||||
case "m2t":
|
||||
case "m2ts":
|
||||
return "MPEG-2 Transport Stream (M2TS)";
|
||||
case "mpeg":
|
||||
return "Moving Picture Experts Group (MPEG)";
|
||||
case "mjpg":
|
||||
|
42
go.mod
42
go.mod
@@ -13,9 +13,9 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/esimov/pigo v1.4.6
|
||||
github.com/gin-contrib/gzip v1.2.3
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/golang/geo v0.0.0-20250509130527-0a13e5a5d53d
|
||||
github.com/google/open-location-code/go v0.0.0-20250415120251-fa6d7f9d4765
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/golang/geo v0.0.0-20250606134707-e8fe6a72b492
|
||||
github.com/google/open-location-code/go v0.0.0-20250523152404-3cf9f806af4d
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/gosimple/slug v1.15.0
|
||||
github.com/jinzhu/gorm v1.9.16
|
||||
@@ -24,7 +24,7 @@ require (
|
||||
github.com/karrick/godirwalk v1.17.0
|
||||
github.com/klauspost/cpuid/v2 v2.2.10
|
||||
github.com/leandro-lugaresi/hub v1.1.1
|
||||
github.com/leonelquinteros/gotext v1.7.1
|
||||
github.com/leonelquinteros/gotext v1.7.2
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mandykoh/prism v0.35.3
|
||||
@@ -40,15 +40,15 @@ require (
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/net v0.40.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/net v0.41.0
|
||||
gonum.org/v1/gonum v0.16.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||
golang.org/x/image v0.27.0
|
||||
golang.org/x/image v0.28.0
|
||||
)
|
||||
|
||||
require github.com/olekukonko/tablewriter v0.0.5
|
||||
@@ -60,34 +60,34 @@ require github.com/chzyer/readline v1.5.1 // indirect
|
||||
require github.com/gabriel-vasile/mimetype v1.4.9
|
||||
|
||||
require (
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/time v0.11.0
|
||||
golang.org/x/sync v0.15.0
|
||||
golang.org/x/time v0.12.0
|
||||
)
|
||||
|
||||
require github.com/go-ldap/ldap/v3 v3.4.11
|
||||
|
||||
require (
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/prometheus/common v0.63.0
|
||||
github.com/prometheus/common v0.64.0
|
||||
)
|
||||
|
||||
require github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0
|
||||
|
||||
require golang.org/x/text v0.25.0
|
||||
require golang.org/x/text v0.26.0
|
||||
|
||||
require (
|
||||
github.com/IGLOU-EU/go-wildcard v1.0.3
|
||||
github.com/davidbyttow/govips/v2 v2.16.0
|
||||
github.com/go-co-op/gocron/v2 v2.16.1
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/go-co-op/gocron/v2 v2.16.2
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/ugjka/go-tz/v2 v2.2.6
|
||||
github.com/urfave/cli/v2 v2.27.6
|
||||
github.com/wamuir/graft v0.10.0
|
||||
github.com/zitadel/oidc/v3 v3.38.1
|
||||
golang.org/x/mod v0.24.0
|
||||
github.com/zitadel/oidc/v3 v3.39.0
|
||||
golang.org/x/mod v0.25.0
|
||||
golang.org/x/sys v0.33.0
|
||||
)
|
||||
|
||||
@@ -132,7 +132,7 @@ require (
|
||||
github.com/muhlemmer/gu v0.3.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/procfs v0.16.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
@@ -140,7 +140,7 @@ require (
|
||||
github.com/swaggo/swag v1.16.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/ugorji/go/codec v1.2.14 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
github.com/zitadel/logging v0.6.2 // indirect
|
||||
github.com/zitadel/schema v1.3.1 // indirect
|
||||
@@ -148,20 +148,20 @@ require (
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
golang.org/x/oauth2 v0.29.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/abema/go-mp4 v1.4.1
|
||||
github.com/bytedance/sonic v1.13.2 // indirect
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/sunfish-shogi/bufseekio v0.1.0
|
||||
golang.org/x/arch v0.16.0 // indirect
|
||||
golang.org/x/arch v0.18.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
|
84
go.sum
84
go.sum
@@ -38,8 +38,8 @@ github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
|
||||
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
@@ -127,14 +127,14 @@ github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2
|
||||
github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-co-op/gocron/v2 v2.16.1 h1:ux/5zxVRveCaCuTtNI3DiOk581KC1KpJbpJFYUEVYwo=
|
||||
github.com/go-co-op/gocron/v2 v2.16.1/go.mod h1:opexeOFy5BplhsKdA7bzY9zeYih8I8/WNJ4arTIFPVc=
|
||||
github.com/go-co-op/gocron/v2 v2.16.2 h1:r08P663ikXiulLT9XaabkLypL/W9MoCIbqgQoAutyX4=
|
||||
github.com/go-co-op/gocron/v2 v2.16.2/go.mod h1:4YTLGCCAH75A5RlQ6q+h+VacO7CgjkgP0EJ+BEOXRSI=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
@@ -182,8 +182,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20250509130527-0a13e5a5d53d h1:744gh8J7sbiKoLDyb4KMKj7DCiA2+vDVjb/nKGiv6yE=
|
||||
github.com/golang/geo v0.0.0-20250509130527-0a13e5a5d53d/go.mod h1:Vaw7L5b+xa3Rj4/pRtrQkymn3lSBRB/NAEdbF9YEVLA=
|
||||
github.com/golang/geo v0.0.0-20250606134707-e8fe6a72b492 h1:8mHyM6CCmj/DQAhHXJVTgdkg/6hAH71N7qGEF+t4Bzg=
|
||||
github.com/golang/geo v0.0.0-20250606134707-e8fe6a72b492/go.mod h1:Vaw7L5b+xa3Rj4/pRtrQkymn3lSBRB/NAEdbF9YEVLA=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -209,8 +209,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/open-location-code/go v0.0.0-20250415120251-fa6d7f9d4765 h1:/Xn4RiCibwInR8cebnpm/muVubIUBbQcMIovW3omkQE=
|
||||
github.com/google/open-location-code/go v0.0.0-20250415120251-fa6d7f9d4765/go.mod h1:eJfRN6aj+kR/rnua/rw9jAgYhqoMHldQkdTi+sePRKk=
|
||||
github.com/google/open-location-code/go v0.0.0-20250523152404-3cf9f806af4d h1:Vsgdb0N5xlbjgshQmoprjFB+noSo0mioy4Xdl4rk3fg=
|
||||
github.com/google/open-location-code/go v0.0.0-20250523152404-3cf9f806af4d/go.mod h1:eJfRN6aj+kR/rnua/rw9jAgYhqoMHldQkdTi+sePRKk=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
@@ -286,8 +286,8 @@ github.com/leandro-lugaresi/hub v1.1.1 h1:zqp0HzFvj4HtqjMBXM2QF17o6PNmR8MJOChgeK
|
||||
github.com/leandro-lugaresi/hub v1.1.1/go.mod h1:XEFWanhHv6Rt3XlteHMxuNDYi8dJcpJjodpqkU+BtIo=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/leonelquinteros/gotext v1.7.1 h1:/JNPeE3lY5JeVYv2+KBpz39994W3W9fmZCGq3eO9Ri8=
|
||||
github.com/leonelquinteros/gotext v1.7.1/go.mod h1:I0WoFDn9u2D3VbPnnDPT8mzZu0iSXG8iih+AH2fHHqg=
|
||||
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
|
||||
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
@@ -337,15 +337,15 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
|
||||
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
|
||||
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@@ -399,8 +399,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugjka/go-tz/v2 v2.2.6 h1:xAjw0dwSoLZYVBv1lA+n165ibSnDtHguBQNbeAMDwNE=
|
||||
github.com/ugjka/go-tz/v2 v2.2.6/go.mod h1:Jh35OKbERtwjZLWDZ2KgjD+bm5hb9Lx8nVD9Mv9NVzs=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw=
|
||||
github.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6 h1:TtyC78WMafNW8QFfv3TeP3yWNDG+uxNkk9vOrnDu6JA=
|
||||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6/go.mod h1:h8272+G2omSmi30fBXiZDMkmHuOgonplfKIKjQWzlfs=
|
||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
||||
@@ -412,8 +412,8 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBi
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU=
|
||||
github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4=
|
||||
github.com/zitadel/oidc/v3 v3.38.1 h1:VTf1Bv/33UbSwJnIWbfEIdpUGYKfoHetuBNIqVTcjvA=
|
||||
github.com/zitadel/oidc/v3 v3.38.1/go.mod h1:muukzAasaWmn3vBwEVMglJfuTE0PKCvLJGombPwXIRw=
|
||||
github.com/zitadel/oidc/v3 v3.39.0 h1:WK3eNqmgshiYo1oEqONfXXbPbve+Qzgjl8KhKDFUvxc=
|
||||
github.com/zitadel/oidc/v3 v3.39.0/go.mod h1:JwdgdU/WxkmBtWuE8/pEjAbDTWXxJGqBix/gUoeEig4=
|
||||
github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU=
|
||||
github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
@@ -433,8 +433,8 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
||||
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@@ -446,8 +446,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -462,8 +462,8 @@ golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+o
|
||||
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
||||
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -484,8 +484,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -516,15 +516,15 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -536,8 +536,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -594,12 +594,12 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -628,8 +628,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -13,9 +14,11 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg"
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/photoprism/dl"
|
||||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||
"github.com/photoprism/photoprism/internal/photoprism/ytdl"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
@@ -108,12 +111,17 @@ func downloadAction(ctx *cli.Context) error {
|
||||
mediaType = media.Video
|
||||
log.Infof("downloading %s from %s", mediaType, clean.Log(sourceUrl.String()))
|
||||
|
||||
result, err := ytdl.New(context.Background(), sourceUrl.String(), ytdl.Options{
|
||||
MergeOutputFormat: "mp4",
|
||||
RemuxVideo: "mp4",
|
||||
SortingFormat: "lang,quality,res,fps,hdr:10+,vcodec:h264>av01>h265>vp9.2>vp9>h263,acodec:m4a>mp4a>aac>mp3>mp3>ac3>dts,channels,size,br,asr,proto,ext,hasaud,source,id",
|
||||
PlaylistStart: 1,
|
||||
})
|
||||
opt := dl.Options{
|
||||
// The following flags currently seem to have no effect when piping the output to stdout;
|
||||
// however, that may change in a future version of the "yt-dlp" video downloader:
|
||||
MergeOutputFormat: fs.VideoMp4.String(),
|
||||
RemuxVideo: fs.VideoMp4.String(),
|
||||
// Alternative codec sorting format to prioritize H264/AVC:
|
||||
// vcodec:h264>av01>h265>vp9.2>vp9>h263,acodec:m4a>mp4a>aac>mp3>mp3>ac3>dts
|
||||
SortingFormat: "lang,quality,res,fps,codec:avc:m4a,channels,size,br,asr,proto,ext,hasaud,source,id",
|
||||
}
|
||||
|
||||
result, err := dl.NewMetadata(context.Background(), sourceUrl.String(), opt)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -130,14 +138,22 @@ func downloadAction(ctx *cli.Context) error {
|
||||
|
||||
// Download the first video and embed its metadata,
|
||||
// see https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#format-selection-examples.
|
||||
downloadResult, err := result.DownloadWithOptions(context.Background(), ytdl.DownloadOptions{
|
||||
// Filter: "bv*+ba/b",
|
||||
downloadResult, err := result.DownloadWithOptions(context.Background(), dl.DownloadOptions{
|
||||
// TODO: While this may work with a future version of the "yt-dlp" video downloader,
|
||||
// it is currently not possible to properly download videos with separate video and
|
||||
// audio streams when piping the output to stdout. For now, the following Filter
|
||||
// will download the best combined video and audio content (see docs for details).
|
||||
Filter: "best",
|
||||
// Alternative filters for combining the best video and audio streams:
|
||||
// Filter: "bestvideo*+bestaudio/best",
|
||||
// Filter: "best/bestvideo+bestaudio",
|
||||
DownloadAudioOnly: false,
|
||||
EmbedMetadata: true,
|
||||
EmbedSubs: false,
|
||||
ForceOverwrites: true,
|
||||
ForceOverwrites: false,
|
||||
DisableCaching: false,
|
||||
PlaylistIndex: 1,
|
||||
// Download the first video if multiple videos are available:
|
||||
PlaylistIndex: 1,
|
||||
})
|
||||
|
||||
// Check if download was successful.
|
||||
@@ -159,6 +175,44 @@ func downloadAction(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
file.Close()
|
||||
|
||||
// TODO: The remux command flags currently don't seem to have an effect when piping the output to stdout,
|
||||
// so this command will manually remux the downloaded file with ffmpeg. This ensures that the file is a
|
||||
// valid MP4 that can be played. It also adds metadata in the same step.
|
||||
remuxOpt := encode.NewRemuxOptions(conf.FFmpegBin(), fs.VideoMp4, false)
|
||||
|
||||
if title := clean.Name(result.Info.Title); title != "" {
|
||||
remuxOpt.Title = title
|
||||
} else if title = clean.Name(result.Info.AltTitle); title != "" {
|
||||
remuxOpt.Title = title
|
||||
}
|
||||
|
||||
if desc := strings.TrimSpace(result.Info.Description); desc != "" {
|
||||
remuxOpt.Description = desc
|
||||
}
|
||||
|
||||
if u := strings.TrimSpace(sourceUrl.String()); u != "" {
|
||||
remuxOpt.Comment = u
|
||||
}
|
||||
|
||||
if author := clean.Name(result.Info.Artist); author != "" {
|
||||
remuxOpt.Author = author
|
||||
} else if author = clean.Name(result.Info.AlbumArtist); author != "" {
|
||||
remuxOpt.Author = author
|
||||
} else if author = clean.Name(result.Info.Creator); author != "" {
|
||||
remuxOpt.Author = author
|
||||
} else if author = clean.Name(result.Info.License); author != "" {
|
||||
remuxOpt.Author = author
|
||||
}
|
||||
|
||||
if result.Info.Timestamp > 1 {
|
||||
sec, dec := math.Modf(result.Info.Timestamp)
|
||||
remuxOpt.Created = time.Unix(int64(sec), int64(dec*(1e9)))
|
||||
}
|
||||
|
||||
if remuxErr := ffmpeg.RemuxFile(downloadFilePath, "", remuxOpt); remuxErr != nil {
|
||||
return remuxErr
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("importing %s to %s", mediaType, clean.Log(filepath.Join(conf.OriginalsPath(), destFolder)))
|
||||
|
@@ -51,6 +51,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/config/ttl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
"github.com/photoprism/photoprism/internal/photoprism/dl"
|
||||
"github.com/photoprism/photoprism/internal/service/hub"
|
||||
"github.com/photoprism/photoprism/internal/service/hub/places"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
@@ -278,6 +279,11 @@ func (c *Config) Propagate() {
|
||||
thumb.CachePublic = c.HttpCachePublic()
|
||||
initThumbs()
|
||||
|
||||
// Configure video download package.
|
||||
dl.YtDlpBin = c.YtDlpBin()
|
||||
dl.FFmpegBin = c.FFmpegBin()
|
||||
dl.FFprobeBin = c.FFprobeBin()
|
||||
|
||||
// Configure computer vision package.
|
||||
vision.AssetsPath = c.AssetsPath()
|
||||
vision.FaceNetModelPath = c.FaceNetModelPath()
|
||||
|
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
@@ -14,6 +15,20 @@ func (c *Config) FFmpegBin() string {
|
||||
return FindBin(c.options.FFmpegBin, encode.FFmpegBin)
|
||||
}
|
||||
|
||||
// FFprobeBin returns the ffprobe executable file name.
|
||||
func (c *Config) FFprobeBin() string {
|
||||
if ffmpegBin := c.FFmpegBin(); ffmpegBin != "" && fs.FileExistsNotEmpty(ffmpegBin) {
|
||||
return FindBin(filepath.Join(filepath.Dir(ffmpegBin), encode.FFprobeBin), encode.FFprobeBin)
|
||||
}
|
||||
|
||||
return FindBin(encode.FFprobeBin)
|
||||
}
|
||||
|
||||
// YtDlpBin returns the name of the video download executable file, if installed.
|
||||
func (c *Config) YtDlpBin() string {
|
||||
return FindBin("yt-dlp", "yt-dl", "youtube-dl", "dl")
|
||||
}
|
||||
|
||||
// FFmpegEnabled checks if FFmpeg is enabled for video transcoding.
|
||||
func (c *Config) FFmpegEnabled() bool {
|
||||
return !c.DisableFFmpeg()
|
||||
|
@@ -99,7 +99,6 @@ func TestConfig_UserUploadPath(t *testing.T) {
|
||||
|
||||
func TestConfig_SidecarPathIsAbs(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, true, c.SidecarPathIsAbs())
|
||||
c.options.SidecarPath = ".photoprism"
|
||||
assert.Equal(t, false, c.SidecarPathIsAbs())
|
||||
@@ -107,16 +106,24 @@ func TestConfig_SidecarPathIsAbs(t *testing.T) {
|
||||
|
||||
func TestConfig_SidecarWritable(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.Equal(t, true, c.SidecarWritable())
|
||||
}
|
||||
|
||||
func TestConfig_FFmpegBin(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
assert.True(t, strings.Contains(c.FFmpegBin(), "/bin/ffmpeg"))
|
||||
}
|
||||
|
||||
func TestConfig_FFprobeBin(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
assert.True(t, strings.Contains(c.FFprobeBin(), "/bin/ffprobe"))
|
||||
}
|
||||
|
||||
func TestConfig_YtDlpBin(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
assert.True(t, strings.Contains(c.YtDlpBin(), "/bin/yt-dlp"))
|
||||
}
|
||||
|
||||
func TestConfig_TempPath(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
|
@@ -8,23 +8,24 @@ import (
|
||||
|
||||
// TranscodeToAvcCmd returns the FFmpeg command for hardware-accelerated transcoding to MPEG-4 AVC.
|
||||
func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
// ffmpeg -hide_banner -h encoder=h264_videotoolbox
|
||||
return exec.Command(
|
||||
opt.Bin,
|
||||
"-hide_banner", "-y",
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-strict", "-2",
|
||||
"-i", srcName,
|
||||
"-c:v", opt.Encoder.String(),
|
||||
"-map", opt.MapVideo,
|
||||
"-map", opt.MapAudio,
|
||||
"-ignore_unknown",
|
||||
"-c:a", "aac",
|
||||
"-vf", opt.VideoFilter(encode.FormatYUV420P),
|
||||
"-profile", "high",
|
||||
"-level", "51",
|
||||
"-r", "30",
|
||||
"-q:v", opt.QvQuality(),
|
||||
"-f", "mp4",
|
||||
"-movflags", opt.MovFlags,
|
||||
"-map_metadata", opt.MapMetadata,
|
||||
destName,
|
||||
)
|
||||
}
|
||||
|
@@ -6,20 +6,22 @@ import "os/exec"
|
||||
func TranscodeToAvcCmd(srcName, destName string, opt Options) *exec.Cmd {
|
||||
return exec.Command(
|
||||
opt.Bin,
|
||||
"-hide_banner", "-y",
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-strict", "-2",
|
||||
"-i", srcName,
|
||||
"-c:v", opt.Encoder.String(),
|
||||
"-map", opt.MapVideo,
|
||||
"-map", opt.MapAudio,
|
||||
"-ignore_unknown",
|
||||
"-c:a", "aac",
|
||||
"-preset", opt.Preset,
|
||||
"-vf", opt.VideoFilter(FormatYUV420P),
|
||||
"-max_muxing_queue_size", "1024",
|
||||
"-r", "30",
|
||||
"-crf", opt.CrfQuality(),
|
||||
"-f", "mp4",
|
||||
"-movflags", opt.MovFlags,
|
||||
"-map_metadata", opt.MapMetadata,
|
||||
destName,
|
||||
)
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ const (
|
||||
|
||||
// Default video and audio track mapping.
|
||||
const (
|
||||
DefaultMapVideo = "0:v:0"
|
||||
DefaultMapAudio = "0:a:0?"
|
||||
DefaultMapVideo = "0:v:0"
|
||||
DefaultMapAudio = "0:a:0?"
|
||||
DefaultMapMetadata = "0"
|
||||
)
|
||||
|
@@ -6,4 +6,4 @@ package encode
|
||||
// - https://cloudinary.com/glossary/fragmented-mp4
|
||||
// - https://medium.com/@vlad.pbr/in-browser-live-video-using-fragmented-mp4-3aedb600a07e
|
||||
// - https://github.com/video-dev/hls.js?tab=readme-ov-file#features
|
||||
var MovFlags = "frag_keyframe+empty_moov+default_base_moof+faststart"
|
||||
var MovFlags = "use_metadata_tags+faststart"
|
||||
|
@@ -3,21 +3,31 @@ package encode
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
// Options represents FFmpeg encoding options.
|
||||
type Options struct {
|
||||
Bin string // FFmpeg binary filename, e.g. /usr/bin/ffmpeg
|
||||
Encoder Encoder // Supported FFmpeg output Encoder
|
||||
SizeLimit int // Maximum width and height of the output video file in pixels.
|
||||
Quality int // See https://ffmpeg.org/ffmpeg-codecs.html
|
||||
Preset string // See https://trac.ffmpeg.org/wiki/Encode/H.264#Preset
|
||||
Device string // See https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
|
||||
MapVideo string // See https://trac.ffmpeg.org/wiki/Map#Videostreamsonly
|
||||
MapAudio string // See https://trac.ffmpeg.org/wiki/Map#Audiostreamsonly
|
||||
TimeOffset string // See https://trac.ffmpeg.org/wiki/Seeking and https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax
|
||||
Duration time.Duration // See https://ffmpeg.org/ffmpeg.html#Main-options
|
||||
MovFlags string
|
||||
Bin string // FFmpeg binary filename, e.g. /usr/bin/ffmpeg
|
||||
Container fs.Type // Multimedia Container File Format
|
||||
Encoder Encoder // Supported FFmpeg output Encoder
|
||||
SizeLimit int // Maximum width and height of the output video file in pixels.
|
||||
Quality int // See https://ffmpeg.org/ffmpeg-codecs.html
|
||||
Preset string // See https://trac.ffmpeg.org/wiki/Encode/H.264#Preset
|
||||
Device string // See https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
|
||||
MapVideo string // See https://trac.ffmpeg.org/wiki/Map#Videostreamsonly
|
||||
MapAudio string // See https://trac.ffmpeg.org/wiki/Map#Audiostreamsonly
|
||||
MapMetadata string // See https://ffmpeg.org/ffmpeg.html
|
||||
TimeOffset string // See https://trac.ffmpeg.org/wiki/Seeking and https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax
|
||||
Duration time.Duration // See https://ffmpeg.org/ffmpeg.html#Main-options
|
||||
MovFlags string
|
||||
Title string
|
||||
Description string
|
||||
Comment string
|
||||
Author string
|
||||
Created time.Time
|
||||
Force bool
|
||||
}
|
||||
|
||||
// NewVideoOptions creates and returns new FFmpeg video transcoding options.
|
||||
@@ -57,15 +67,38 @@ func NewVideoOptions(ffmpegBin string, encoder Encoder, sizeLimit, quality int,
|
||||
}
|
||||
|
||||
return Options{
|
||||
Bin: ffmpegBin,
|
||||
Encoder: encoder,
|
||||
SizeLimit: sizeLimit,
|
||||
Quality: quality,
|
||||
Preset: preset,
|
||||
Device: device,
|
||||
MapVideo: mapVideo,
|
||||
MapAudio: mapAudio,
|
||||
MovFlags: MovFlags,
|
||||
Bin: ffmpegBin,
|
||||
Container: fs.VideoMp4,
|
||||
Encoder: encoder,
|
||||
SizeLimit: sizeLimit,
|
||||
Quality: quality,
|
||||
Preset: preset,
|
||||
Device: device,
|
||||
MapVideo: mapVideo,
|
||||
MapAudio: mapAudio,
|
||||
MapMetadata: DefaultMapMetadata,
|
||||
MovFlags: MovFlags,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRemuxOptions creates and returns new video remux options.
|
||||
func NewRemuxOptions(ffmpegBin string, container fs.Type, force bool) Options {
|
||||
if ffmpegBin == "" {
|
||||
ffmpegBin = FFmpegBin
|
||||
}
|
||||
|
||||
if container == "" {
|
||||
container = fs.VideoMp4
|
||||
}
|
||||
|
||||
return Options{
|
||||
Bin: ffmpegBin,
|
||||
Container: fs.VideoMp4,
|
||||
MapVideo: DefaultMapVideo,
|
||||
MapAudio: DefaultMapAudio,
|
||||
MapMetadata: DefaultMapMetadata,
|
||||
MovFlags: MovFlags,
|
||||
Force: force,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,8 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
if opt.Device != "" {
|
||||
return exec.Command(
|
||||
opt.Bin,
|
||||
"-hide_banner", "-y",
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-strict", "-2",
|
||||
"-hwaccel", "qsv",
|
||||
"-hwaccel_device", opt.Device,
|
||||
@@ -23,17 +24,19 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
"-c:v", opt.Encoder.String(),
|
||||
"-map", opt.MapVideo,
|
||||
"-map", opt.MapAudio,
|
||||
"-ignore_unknown",
|
||||
"-preset", opt.Preset,
|
||||
"-r", "30",
|
||||
"-global_quality", opt.GlobalQuality(),
|
||||
"-f", "mp4",
|
||||
"-movflags", opt.MovFlags,
|
||||
"-map_metadata", opt.MapMetadata,
|
||||
destName,
|
||||
)
|
||||
} else {
|
||||
return exec.Command(
|
||||
opt.Bin,
|
||||
"-hide_banner", "-y",
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-strict", "-2",
|
||||
"-hwaccel", "qsv",
|
||||
"-hwaccel_output_format", "qsv",
|
||||
@@ -43,11 +46,12 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
"-c:v", opt.Encoder.String(),
|
||||
"-map", opt.MapVideo,
|
||||
"-map", opt.MapAudio,
|
||||
"-ignore_unknown",
|
||||
"-preset", opt.Preset,
|
||||
"-r", "30",
|
||||
"-global_quality", opt.GlobalQuality(),
|
||||
"-f", "mp4",
|
||||
"-movflags", opt.MovFlags,
|
||||
"-map_metadata", opt.MapMetadata,
|
||||
destName,
|
||||
)
|
||||
}
|
||||
|
@@ -11,7 +11,8 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
// ffmpeg -hide_banner -h encoder=h264_nvenc
|
||||
return exec.Command(
|
||||
opt.Bin,
|
||||
"-hide_banner", "-y",
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-strict", "-2",
|
||||
"-hwaccel", "auto",
|
||||
"-i", srcName,
|
||||
@@ -19,6 +20,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
"-c:v", opt.Encoder.String(),
|
||||
"-map", opt.MapVideo,
|
||||
"-map", opt.MapAudio,
|
||||
"-ignore_unknown",
|
||||
"-c:a", "aac",
|
||||
"-preset", opt.Preset,
|
||||
"-pixel_format", "yuv420p",
|
||||
@@ -27,12 +29,12 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
"-rc:v", "constqp",
|
||||
"-cq", opt.CqQuality(),
|
||||
"-tune", "2",
|
||||
"-r", "30",
|
||||
"-profile:v", "1",
|
||||
"-level:v", "auto",
|
||||
"-coder:v", "1",
|
||||
"-f", "mp4",
|
||||
"-movflags", opt.MovFlags,
|
||||
"-map_metadata", opt.MapMetadata,
|
||||
destName,
|
||||
)
|
||||
}
|
||||
|
204
internal/ffmpeg/remux.go
Normal file
204
internal/ffmpeg/remux.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
// RemuxFile changes the file format to the specified container as needed.
|
||||
func RemuxFile(videoFilePath, destFilePath string, opt encode.Options) error {
|
||||
// Return if destination file already exists and force option is not set.
|
||||
if !opt.Force && fs.FileExistsNotEmpty(destFilePath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Error if source file does not exist or is empty.
|
||||
if !fs.FileExistsNotEmpty(videoFilePath) {
|
||||
return errors.New("invalid video file path")
|
||||
}
|
||||
|
||||
// Use MP4 as default container format.
|
||||
if opt.Container == "" {
|
||||
opt.Container = fs.ExtMp4
|
||||
}
|
||||
|
||||
videoBaseName := filepath.Base(videoFilePath)
|
||||
|
||||
if destFilePath == "" {
|
||||
destFilePath = fs.StripKnownExt(videoFilePath) + opt.Container.DefaultExt()
|
||||
}
|
||||
|
||||
destFileBase := filepath.Base(destFilePath)
|
||||
destPathName := filepath.Dir(destFilePath)
|
||||
|
||||
tempBaseName := "." + fs.StripKnownExt(clean.FileName(videoBaseName)) + opt.Container.DefaultExt()
|
||||
tempFilePath := filepath.Join(destPathName, tempBaseName)
|
||||
|
||||
cmd, err := RemuxCmd(videoFilePath, tempFilePath, opt)
|
||||
|
||||
// Return if an error occurred.
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if target file already exists.
|
||||
if fs.FileExists(tempFilePath) {
|
||||
if !opt.Force {
|
||||
return fmt.Errorf("temp file %s already exists", clean.Log(tempBaseName))
|
||||
} else if err = os.Remove(tempFilePath); err != nil {
|
||||
return fmt.Errorf("%s (remove temp file)", err)
|
||||
}
|
||||
|
||||
log.Infof("ffmpeg: replacing temp file %s", clean.Log(tempBaseName))
|
||||
}
|
||||
|
||||
// Fetch command output.
|
||||
var out bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Env = append(cmd.Env, []string{
|
||||
fmt.Sprintf("HOME=%s", tempFilePath),
|
||||
}...)
|
||||
|
||||
log.Infof("ffmpeg: changing container format of %s to %s", clean.Log(videoBaseName), opt.Container)
|
||||
|
||||
// Log exact command for debugging in trace mode.
|
||||
log.Trace(cmd.String())
|
||||
|
||||
// Transcode source media file to AVC.
|
||||
start := time.Now()
|
||||
if err = cmd.Run(); err != nil {
|
||||
if stderr.String() != "" {
|
||||
err = errors.New(stderr.String())
|
||||
}
|
||||
|
||||
// Log ffmpeg output for debugging.
|
||||
if err.Error() != "" {
|
||||
log.Debug(err)
|
||||
}
|
||||
|
||||
// Log filename and transcoding time.
|
||||
log.Warnf("ffmpeg: failed to convert %s [%s]", clean.Log(videoBaseName), time.Since(start))
|
||||
|
||||
// Remove broken video file.
|
||||
if !fs.FileExists(tempFilePath) {
|
||||
// Do nothing.
|
||||
} else if err = os.Remove(tempFilePath); err != nil {
|
||||
return fmt.Errorf("failed to remove temp file %s (%s)", clean.Log(tempBaseName), err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Abort if destination file is missing or empty.
|
||||
if !fs.FileExistsNotEmpty(tempFilePath) {
|
||||
_ = os.Remove(tempFilePath)
|
||||
return fmt.Errorf("failed change container format of %s [%s]", clean.Log(videoBaseName), time.Since(start))
|
||||
}
|
||||
|
||||
if !fs.FileExists(destFilePath) {
|
||||
// Do nothing.
|
||||
} else if err = os.Remove(destFilePath); err != nil {
|
||||
_ = os.Remove(tempFilePath)
|
||||
return fmt.Errorf("failed to remove %s (%s)", clean.Log(destFileBase), err)
|
||||
}
|
||||
|
||||
if err = os.Rename(tempFilePath, destFilePath); err != nil {
|
||||
return fmt.Errorf("failed to rename %s to %s (%s)", clean.Log(tempBaseName), clean.Log(destFileBase), err)
|
||||
}
|
||||
|
||||
// Log filename and remux time.
|
||||
if videoBaseName != destFileBase {
|
||||
log.Infof("ffmpeg: converted %s to %s [%s]", clean.Log(videoBaseName), clean.Log(destFileBase), time.Since(start))
|
||||
} else {
|
||||
log.Infof("ffmpeg: converted %s to %s [%s]", clean.Log(videoBaseName), opt.Container.String(), time.Since(start))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemuxCmd returns the FFmpeg command for transferring content from one container format to another without altering the original video or audio stream.
|
||||
func RemuxCmd(srcName, destName string, opt encode.Options) (cmd *exec.Cmd, err error) {
|
||||
if srcName == "" {
|
||||
return nil, fmt.Errorf("empty source filename")
|
||||
} else if !fs.FileExistsNotEmpty(srcName) {
|
||||
return nil, fmt.Errorf("source file is empty or missing")
|
||||
} else if destName == "" {
|
||||
return nil, fmt.Errorf("empty destination filename")
|
||||
} else if srcName == destName {
|
||||
return nil, fmt.Errorf("source and destination filenames must be different")
|
||||
}
|
||||
|
||||
// Use the default binary name if no name is specified.
|
||||
if opt.Bin == "" {
|
||||
opt.Bin = encode.FFmpegBin
|
||||
}
|
||||
|
||||
// Compose "ffmpeg" command flags:
|
||||
flags := []string{
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-strict", "-2",
|
||||
"-avoid_negative_ts", "make_non_negative",
|
||||
"-i", srcName,
|
||||
"-map", opt.MapVideo,
|
||||
"-map", opt.MapAudio,
|
||||
"-dn", // Exclude data streams such as subtitles, timecode tracks, or camera motion data from the output file.
|
||||
"-ignore_unknown",
|
||||
"-codec", "copy",
|
||||
"-f", opt.Container.String(),
|
||||
}
|
||||
|
||||
// Append format specific "ffmpeg" command flags.
|
||||
switch opt.Container {
|
||||
case fs.VideoMp4:
|
||||
// Ensure MP4 compatibility:
|
||||
flags = append(flags,
|
||||
"-movflags", opt.MovFlags,
|
||||
"-map_metadata", opt.MapMetadata, // Copy existing video metadata.
|
||||
)
|
||||
|
||||
// If specified, add the following metadata:
|
||||
if title := clean.Name(opt.Title); title != "" {
|
||||
flags = append(flags, "-metadata", fmt.Sprintf(`title=%s`, title))
|
||||
}
|
||||
|
||||
if desc := strings.TrimSpace(opt.Description); desc != "" {
|
||||
flags = append(flags, "-metadata", fmt.Sprintf(`description=%s`, desc))
|
||||
}
|
||||
|
||||
if comment := strings.TrimSpace(opt.Comment); comment != "" {
|
||||
flags = append(flags, "-metadata", fmt.Sprintf(`comment=%s`, comment))
|
||||
}
|
||||
|
||||
if author := clean.Name(opt.Author); author != "" {
|
||||
flags = append(flags, "-metadata", fmt.Sprintf(`author=%s`, author))
|
||||
}
|
||||
|
||||
if !opt.Created.IsZero() {
|
||||
flags = append(flags, "-metadata", fmt.Sprintf(`creation_time=%s`, opt.Created.Format(time.DateTime)))
|
||||
}
|
||||
}
|
||||
|
||||
// Set the destination file name as the last command flag.
|
||||
flags = append(flags, destName)
|
||||
|
||||
cmd = exec.Command(
|
||||
opt.Bin,
|
||||
flags...,
|
||||
)
|
||||
|
||||
return cmd, nil
|
||||
}
|
100
internal/ffmpeg/remux_test.go
Normal file
100
internal/ffmpeg/remux_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
func TestRemuxFile(t *testing.T) {
|
||||
ffmpegBin := "/usr/bin/ffmpeg"
|
||||
|
||||
t.Run("NoFilePath", func(t *testing.T) {
|
||||
opt := encode.NewRemuxOptions(ffmpegBin, fs.VideoMp4, false)
|
||||
err := RemuxFile("", "", opt)
|
||||
|
||||
assert.Equal(t, "invalid video file path", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Mp4", func(t *testing.T) {
|
||||
opt := encode.NewRemuxOptions(ffmpegBin, fs.VideoMp4, false)
|
||||
|
||||
// QuickTime MOV container with HVC1 (HEVC) codec.
|
||||
origName := fs.Abs("./testdata/30fps.mov")
|
||||
srcName := fs.Abs("./testdata/30fps.remux-file.mov")
|
||||
tmpName := fs.Abs("./testdata/.30fps.remux-file.mp4")
|
||||
destName := fs.Abs("./testdata/30fps.remux-file.avc")
|
||||
|
||||
_ = os.Remove(srcName)
|
||||
_ = os.Remove(tmpName)
|
||||
_ = os.Remove(destName)
|
||||
|
||||
defer func() {
|
||||
_ = os.Remove(srcName)
|
||||
_ = os.Remove(tmpName)
|
||||
_ = os.Remove(destName)
|
||||
}()
|
||||
|
||||
if err := fs.Copy(origName, srcName); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := RemuxFile(srcName, destName, opt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.FileExists(t, srcName)
|
||||
assert.NoFileExists(t, tmpName)
|
||||
assert.FileExists(t, destName)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemuxCmd(t *testing.T) {
|
||||
ffmpegBin := "/usr/bin/ffmpeg"
|
||||
|
||||
t.Run("NoSrcName", func(t *testing.T) {
|
||||
opt := encode.NewRemuxOptions(ffmpegBin, fs.VideoMp4, false)
|
||||
_, err := RemuxCmd("", "", opt)
|
||||
|
||||
assert.Equal(t, "empty source filename", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Mp4", func(t *testing.T) {
|
||||
opt := encode.NewRemuxOptions(ffmpegBin, fs.VideoMp4, false)
|
||||
|
||||
// QuickTime MOV container with HVC1 (HEVC) codec.
|
||||
origName := fs.Abs("./testdata/30fps.mov")
|
||||
|
||||
srcName := fs.Abs("./testdata/30fps.remux-cmd.mov")
|
||||
destName := fs.Abs("./testdata/30fps.remux-cmd.mp4")
|
||||
|
||||
_ = os.Remove(srcName)
|
||||
_ = os.Remove(destName)
|
||||
|
||||
defer func() {
|
||||
_ = os.Remove(srcName)
|
||||
_ = os.Remove(destName)
|
||||
}()
|
||||
|
||||
if err := fs.Copy(origName, srcName); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd, err := RemuxCmd(srcName, destName, opt)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmdStr := cmd.String()
|
||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -avoid_negative_ts make_non_negative -i SRC -map 0:v:0 -map 0:a:0? -dn -ignore_unknown -codec copy -f mp4 -movflags use_metadata_tags+faststart -map_metadata 0 DEST", cmdStr)
|
||||
})
|
||||
}
|
@@ -33,9 +33,11 @@ func TranscodeCmd(srcName, destName string, opt encode.Options) (cmd *exec.Cmd,
|
||||
if fs.TypeAnimated[fs.FileType(srcName)] != "" {
|
||||
cmd = exec.Command(
|
||||
opt.Bin,
|
||||
"-hide_banner", "-y",
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-strict", "-2",
|
||||
"-i", srcName,
|
||||
"-ignore_unknown",
|
||||
"-pix_fmt", encode.FormatYUV420P.String(),
|
||||
"-vf", "scale='trunc(iw/2)*2:trunc(ih/2)*2'",
|
||||
"-f", "mp4",
|
||||
|
@@ -34,7 +34,7 @@ func TestTranscodeCmd(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Contains(t, r.String(), "bin/ffmpeg -hide_banner -y -strict -2 -i VID123.gif -pix_fmt yuv420p -vf scale='trunc(iw/2)*2:trunc(ih/2)*2' -f mp4 -movflags +faststart VID123.gif.avc")
|
||||
assert.Contains(t, r.String(), "bin/ffmpeg -hide_banner -y -strict -2 -i VID123.gif -ignore_unknown -pix_fmt yuv420p -vf scale='trunc(iw/2)*2:trunc(ih/2)*2' -f mp4 -movflags +faststart VID123.gif.avc")
|
||||
})
|
||||
t.Run("VP9toAVC", func(t *testing.T) {
|
||||
opt := encode.NewVideoOptions(ffmpegBin, encode.SoftwareAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "")
|
||||
@@ -52,7 +52,7 @@ func TestTranscodeCmd(t *testing.T) {
|
||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -i SRC -c:v libx264 -map 0:v:0 -map 0:a:0? -c:a aac -preset fast -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -max_muxing_queue_size 1024 -r 30 -crf 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -i SRC -c:v libx264 -map 0:v:0 -map 0:a:0? -ignore_unknown -c:a aac -preset fast -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -max_muxing_queue_size 1024 -crf 25 -f mp4 -movflags use_metadata_tags+faststart -map_metadata 0 DEST", cmdStr)
|
||||
|
||||
// Run generated command to test software transcoding.
|
||||
RunCommandTest(t, opt.Encoder, srcName, destName, cmd, true)
|
||||
@@ -73,7 +73,7 @@ func TestTranscodeCmd(t *testing.T) {
|
||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel vaapi -i SRC -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=nv12,hwupload -c:v h264_vaapi -map 0:v:0 -map 0:a:0? -r 30 -qp 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel vaapi -i SRC -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=nv12,hwupload -c:v h264_vaapi -map 0:v:0 -map 0:a:0? -ignore_unknown -qp 25 -f mp4 -movflags use_metadata_tags+faststart -map_metadata 0 DEST", cmdStr)
|
||||
|
||||
// This transcoding test requires a supported hardware device that is properly configured:
|
||||
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "vaapi" {
|
||||
@@ -97,7 +97,7 @@ func TestTranscodeCmd(t *testing.T) {
|
||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel qsv -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format qsv -i SRC -c:a aac -vf scale_qsv=w='if(gte(iw,ih), min(1500, iw), -1)':h='if(gte(iw,ih), -1, min(1500, ih))':format=nv12 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -preset fast -r 30 -global_quality 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel qsv -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format qsv -i SRC -c:a aac -vf scale_qsv=w='if(gte(iw,ih), min(1500, iw), -1)':h='if(gte(iw,ih), -1, min(1500, ih))':format=nv12 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -ignore_unknown -preset fast -global_quality 25 -f mp4 -movflags use_metadata_tags+faststart -map_metadata 0 DEST", cmdStr)
|
||||
|
||||
// This transcoding test requires a supported hardware device that is properly configured:
|
||||
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "intel" {
|
||||
@@ -120,7 +120,7 @@ func TestTranscodeCmd(t *testing.T) {
|
||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel qsv -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format qsv -i SRC -c:a aac -vf scale_qsv=w='if(gte(iw,ih), min(1500, iw), -1)':h='if(gte(iw,ih), -1, min(1500, ih))':format=nv12 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -preset fast -r 30 -global_quality 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel qsv -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format qsv -i SRC -c:a aac -vf scale_qsv=w='if(gte(iw,ih), min(1500, iw), -1)':h='if(gte(iw,ih), -1, min(1500, ih))':format=nv12 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -ignore_unknown -preset fast -global_quality 25 -f mp4 -movflags use_metadata_tags+faststart -map_metadata 0 DEST", cmdStr)
|
||||
|
||||
// This transcoding test requires a supported hardware device that is properly configured:
|
||||
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "intel" {
|
||||
@@ -144,7 +144,7 @@ func TestTranscodeCmd(t *testing.T) {
|
||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel auto -i SRC -pix_fmt yuv420p -c:v h264_nvenc -map 0:v:0 -map 0:a:0? -c:a aac -preset fast -pixel_format yuv420p -gpu any -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -rc:v constqp -cq 25 -tune 2 -r 30 -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel auto -i SRC -pix_fmt yuv420p -c:v h264_nvenc -map 0:v:0 -map 0:a:0? -ignore_unknown -c:a aac -preset fast -pixel_format yuv420p -gpu any -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -rc:v constqp -cq 25 -tune 2 -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags use_metadata_tags+faststart -map_metadata 0 DEST", cmdStr)
|
||||
|
||||
// This transcoding test requires a supported hardware device that is properly configured:
|
||||
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "nvidia" {
|
||||
@@ -167,7 +167,7 @@ func TestTranscodeCmd(t *testing.T) {
|
||||
cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1)
|
||||
cmdStr = strings.Replace(cmdStr, destName, "DEST", 1)
|
||||
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel auto -i SRC -pix_fmt yuv420p -c:v h264_nvenc -map 0:v:0 -map 0:a:0? -c:a aac -preset fast -pixel_format yuv420p -gpu any -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -rc:v constqp -cq 25 -tune 2 -r 30 -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr)
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel auto -i SRC -pix_fmt yuv420p -c:v h264_nvenc -map 0:v:0 -map 0:a:0? -ignore_unknown -c:a aac -preset fast -pixel_format yuv420p -gpu any -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -rc:v constqp -cq 25 -tune 2 -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags use_metadata_tags+faststart -map_metadata 0 DEST", cmdStr)
|
||||
|
||||
// This transcoding test requires a supported hardware device that is properly configured:
|
||||
if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "nvidia" {
|
||||
@@ -182,7 +182,7 @@ func TestTranscodeCmd(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Contains(t, r.String(), "ffmpeg -hide_banner -y -strict -2 -i VID123.mov -c:v h264_videotoolbox -map 0:v:0 -map 0:a:0? -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -profile high -level 51 -r 30 -q:v 50 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart VID123.mov.avc")
|
||||
assert.Contains(t, r.String(), "ffmpeg -hide_banner -y -strict -2 -i VID123.mov -c:v h264_videotoolbox -map 0:v:0 -map 0:a:0? -ignore_unknown -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -profile high -level 51 -q:v 50 -f mp4 -movflags use_metadata_tags+faststart -map_metadata 0 VID123.mov.avc")
|
||||
})
|
||||
t.Run("Video4Linux", func(t *testing.T) {
|
||||
opt := encode.NewVideoOptions("", encode.V4LAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "")
|
||||
@@ -192,6 +192,6 @@ func TestTranscodeCmd(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Contains(t, r.String(), "ffmpeg -hide_banner -y -strict -2 -i VID123.mov -c:v h264_v4l2m2m -map 0:v:0 -map 0:a:0? -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -num_output_buffers 72 -num_capture_buffers 64 -max_muxing_queue_size 1024 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart VID123.mov.avc")
|
||||
assert.Contains(t, r.String(), "ffmpeg -hide_banner -y -strict -2 -i VID123.mov -c:v h264_v4l2m2m -map 0:v:0 -map 0:a:0? -ignore_unknown -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -num_output_buffers 72 -num_capture_buffers 64 -max_muxing_queue_size 1024 -f mp4 -movflags use_metadata_tags+faststart -map_metadata 0 VID123.mov.avc")
|
||||
})
|
||||
}
|
||||
|
@@ -11,12 +11,14 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
// ffmpeg -hide_banner -h encoder=h264_v4l2m2m
|
||||
return exec.Command(
|
||||
opt.Bin,
|
||||
"-hide_banner", "-y",
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-strict", "-2",
|
||||
"-i", srcName,
|
||||
"-c:v", opt.Encoder.String(),
|
||||
"-map", opt.MapVideo,
|
||||
"-map", opt.MapAudio,
|
||||
"-ignore_unknown",
|
||||
"-c:a", "aac",
|
||||
"-vf", opt.VideoFilter(encode.FormatYUV420P),
|
||||
"-num_output_buffers", "72",
|
||||
@@ -24,6 +26,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
"-max_muxing_queue_size", "1024",
|
||||
"-f", "mp4",
|
||||
"-movflags", opt.MovFlags,
|
||||
"-map_metadata", opt.MapMetadata,
|
||||
destName,
|
||||
)
|
||||
}
|
||||
|
@@ -11,7 +11,8 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
if opt.Device != "" {
|
||||
return exec.Command(
|
||||
opt.Bin,
|
||||
"-hide_banner", "-y",
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-strict", "-2",
|
||||
"-hwaccel", "vaapi",
|
||||
"-hwaccel_device", opt.Device,
|
||||
@@ -21,16 +22,18 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
"-c:v", opt.Encoder.String(),
|
||||
"-map", opt.MapVideo,
|
||||
"-map", opt.MapAudio,
|
||||
"-r", "30",
|
||||
"-ignore_unknown",
|
||||
"-qp", opt.QpQuality(),
|
||||
"-f", "mp4",
|
||||
"-movflags", opt.MovFlags,
|
||||
"-map_metadata", opt.MapMetadata,
|
||||
destName,
|
||||
)
|
||||
} else {
|
||||
return exec.Command(
|
||||
opt.Bin,
|
||||
"-hide_banner", "-y",
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-strict", "-2",
|
||||
"-hwaccel", "vaapi",
|
||||
"-i", srcName,
|
||||
@@ -39,10 +42,11 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd {
|
||||
"-c:v", opt.Encoder.String(),
|
||||
"-map", opt.MapVideo,
|
||||
"-map", opt.MapAudio,
|
||||
"-r", "30",
|
||||
"-ignore_unknown",
|
||||
"-qp", opt.QpQuality(),
|
||||
"-f", "mp4",
|
||||
"-movflags", opt.MovFlags,
|
||||
"-map_metadata", opt.MapMetadata,
|
||||
destName,
|
||||
)
|
||||
}
|
||||
|
@@ -19,8 +19,8 @@ type Data struct {
|
||||
MimeType string `meta:"MIMEType" report:"-"`
|
||||
DocumentID string `meta:"ContentIdentifier,MediaGroupUUID,BurstUUID,OriginalDocumentID,DocumentID,ImageUniqueID,DigitalImageGUID"` // see https://exiftool.org/forum/index.php?topic=14874.0
|
||||
InstanceID string `meta:"InstanceID,DocumentID"`
|
||||
CreatedAt time.Time `meta:"SubSecCreateDate,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,TrackCreateDate"`
|
||||
TakenAt time.Time `meta:"SubSecDateTimeOriginal,SubSecDateTimeCreated,DateTimeOriginal,CreationDate,DateTimeCreated,DateTime,DateTimeDigitized" xmp:"DateCreated"`
|
||||
CreatedAt time.Time `meta:"SubSecCreateDate,CreationTime,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,TrackCreateDate"`
|
||||
TakenAt time.Time `meta:"SubSecDateTimeOriginal,SubSecDateTimeCreated,DateTimeOriginal,CreationTime,CreationDate,DateTimeCreated,DateTime,DateTimeDigitized" xmp:"DateCreated,CreationTime"`
|
||||
TakenAtLocal time.Time `meta:"SubSecDateTimeOriginal,SubSecDateTimeCreated,DateTimeOriginal,CreationDate,DateTimeCreated,DateTime,DateTimeDigitized"`
|
||||
TakenGps time.Time `meta:"GPSDateTime,GPSDateStamp"`
|
||||
TakenNs int `meta:"-"`
|
||||
|
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
)
|
||||
|
||||
// ToJson uses exiftool to export metadata to a json file.
|
||||
@@ -30,9 +31,20 @@ func (w *Convert) ToJson(f *MediaFile, force bool) (jsonName string, err error)
|
||||
|
||||
log.Debugf("exiftool: extracting metadata from %s", clean.Log(f.RootRelName()))
|
||||
|
||||
cmd := exec.Command(w.conf.ExifToolBin(), "-n", "-m", "-api", "LargeFileSupport", "-j", f.FileName())
|
||||
// Command arguments.
|
||||
var args []string
|
||||
|
||||
// Fetch command output.
|
||||
// Also extract embedded metadata from videos and live photos.
|
||||
if f.IsVideo() || f.MetaData().MediaType == media.Live {
|
||||
args = []string{"-n", "-ee", "-m", "-api", "LargeFileSupport", "-j", f.FileName()}
|
||||
} else {
|
||||
args = []string{"-n", "-m", "-api", "LargeFileSupport", "-j", f.FileName()}
|
||||
}
|
||||
|
||||
// Compose ExifTool command to run.
|
||||
cmd := exec.Command(w.conf.ExifToolBin(), args...)
|
||||
|
||||
// Command environment, output and errors.
|
||||
var out bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
@@ -45,7 +57,7 @@ func (w *Convert) ToJson(f *MediaFile, force bool) (jsonName string, err error)
|
||||
log.Trace(cmd.String())
|
||||
|
||||
// Run convert command.
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err = cmd.Run(); err != nil {
|
||||
if stderr.String() != "" {
|
||||
return "", errors.New(stderr.String())
|
||||
} else {
|
||||
@@ -54,7 +66,7 @@ func (w *Convert) ToJson(f *MediaFile, force bool) (jsonName string, err error)
|
||||
}
|
||||
|
||||
// Write output to file.
|
||||
if err := os.WriteFile(jsonName, []byte(out.String()), fs.ModeFile); err != nil {
|
||||
if err = os.WriteFile(jsonName, []byte(out.String()), fs.ModeFile); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
@@ -41,16 +41,33 @@ func (w *Convert) ToAvc(f *MediaFile, encoder encode.Encoder, noMutex, force boo
|
||||
if f.IsAnimatedImage() {
|
||||
avcName = fs.VideoMp4.FindFirst(f.FileName(), []string{w.conf.SidecarPath(), fs.PPHiddenPathname}, w.conf.OriginalsPath(), false)
|
||||
} else {
|
||||
// Convert MPEG-2 Transport Stream (M2TS) files to MPEG4 containers.
|
||||
if f.IsM2TS() && w.conf.SidecarWritable() {
|
||||
if mp4Name, mp4Err := fs.FileName(f.FileName(), w.conf.SidecarPath(), w.conf.OriginalsPath(), fs.ExtMp4); mp4Err != nil {
|
||||
return nil, fmt.Errorf("convert: %s in %s (remux)", mp4Err, clean.Log(f.RootRelName()))
|
||||
} else if mp4Err = ffmpeg.RemuxFile(f.FileName(), mp4Name, encode.NewRemuxOptions(conf.FFmpegBin(), fs.VideoMp4, false)); mp4Err != nil {
|
||||
return nil, fmt.Errorf("convert: %s in %s (remux)", err, clean.Log(f.RootRelName()))
|
||||
} else if mp4File, fileErr := NewMediaFile(mp4Name); mp4File == nil || fileErr != nil {
|
||||
log.Warnf("convert: %s could not be converted to mp4", logFileName)
|
||||
} else if jsonErr := mp4File.CreateExifToolJson(w); jsonErr != nil {
|
||||
log.Warnf("convert: %s in %s (create json)", jsonErr, logFileName)
|
||||
} else if jsonErr = mp4File.ReadExifToolJson(); jsonErr != nil {
|
||||
log.Warnf("convert: %s in %s (read json)", jsonErr, logFileName)
|
||||
} else if mp4File.MetaData().CodecAvc() {
|
||||
return mp4File, nil
|
||||
}
|
||||
}
|
||||
|
||||
avcName = fs.VideoAvc.FindFirst(f.FileName(), []string{w.conf.SidecarPath(), fs.PPHiddenPathname}, w.conf.OriginalsPath(), false)
|
||||
}
|
||||
|
||||
mediaFile, err := NewMediaFile(avcName)
|
||||
|
||||
// Return it if an MP4 AVC encoded video file already exists.
|
||||
// Return the AVC-encoded video file if it already exists.
|
||||
if mediaFile == nil || err != nil {
|
||||
// Do nothing.
|
||||
} else if mediaFile.IsVideo() {
|
||||
// Return MP4 AVC encoded video file
|
||||
// Return existing AVC file.
|
||||
log.Debugf("convert: %s has already been transcoded to MPEG-4 AVC", logFileName)
|
||||
return mediaFile, nil
|
||||
}
|
||||
|
@@ -1,29 +1,30 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"os/exec"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg/encode"
|
||||
)
|
||||
|
||||
var (
|
||||
Bin = ""
|
||||
YtDlpBin = ""
|
||||
FFmpegBin = ""
|
||||
FFprobeBin = ""
|
||||
)
|
||||
|
||||
// FindBin returns the YouTube / M38U video downloader binary name.
|
||||
func FindBin() string {
|
||||
if Bin == "" {
|
||||
Bin = config.FindBin("yt-dlp", "yt-dl", "youtube-dl", "dl")
|
||||
// FindYtDlpBin returns the YouTube / M38U video downloader binary name.
|
||||
func FindYtDlpBin() string {
|
||||
if YtDlpBin == "" {
|
||||
YtDlpBin, _ = exec.LookPath("yt-dlp")
|
||||
}
|
||||
|
||||
return Bin
|
||||
return YtDlpBin
|
||||
}
|
||||
|
||||
// FindFFmpegBin returns the "ffmpeg" command binary name.
|
||||
func FindFFmpegBin() string {
|
||||
if FFmpegBin == "" {
|
||||
FFmpegBin = config.FindBin(encode.FFmpegBin)
|
||||
FFmpegBin, _ = exec.LookPath(encode.FFmpegBin)
|
||||
}
|
||||
|
||||
return FFmpegBin
|
||||
@@ -32,7 +33,7 @@ func FindFFmpegBin() string {
|
||||
// FindFFprobeBin returns the "ffprobe" command binary name.
|
||||
func FindFFprobeBin() string {
|
||||
if FFprobeBin == "" {
|
||||
FFprobeBin = config.FindBin(encode.FFprobeBin)
|
||||
FFprobeBin, _ = exec.LookPath(encode.FFprobeBin)
|
||||
}
|
||||
|
||||
return FFprobeBin
|
@@ -1,4 +1,4 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func TestFindBin(t *testing.T) {
|
||||
assert.True(t, strings.Contains(FindBin(), "yt-dlp"), "binary filepath should contain 'yt-dlp'")
|
||||
assert.True(t, strings.Contains(FindYtDlpBin(), "yt-dlp"), "binary filepath should contain 'yt-dlp'")
|
||||
}
|
||||
|
||||
func TestFindFFmpegBin(t *testing.T) {
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Package ytdl provides media download functionality.
|
||||
Package dl provides media download functionality.
|
||||
|
||||
Copyright (c) 2018 - 2025 PhotoPrism UG. All rights reserved.
|
||||
|
||||
@@ -28,7 +28,7 @@ want to support our work, or just want to say hello.
|
||||
Additional information can be found in our Developer Guide:
|
||||
<https://docs.photoprism.app/developer-guide/>
|
||||
*/
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/event"
|
@@ -1,4 +1,4 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -40,7 +40,7 @@ func TestParseInfo(t *testing.T) {
|
||||
} {
|
||||
t.Run(c.url, func(t *testing.T) {
|
||||
ctx, cancelFn := context.WithCancel(context.Background())
|
||||
ydlResult, err := New(ctx, c.url, Options{
|
||||
ydlResult, err := NewMetadata(ctx, c.url, Options{
|
||||
DownloadThumbnail: true,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -88,7 +88,7 @@ func TestParseInfo(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPlaylist(t *testing.T) {
|
||||
ydlResult, ydlResultErr := New(context.Background(), playlistRawURL, Options{
|
||||
ydlResult, ydlResultErr := NewMetadata(context.Background(), playlistRawURL, Options{
|
||||
Type: TypePlaylist,
|
||||
DownloadThumbnail: false,
|
||||
})
|
||||
@@ -116,7 +116,7 @@ func TestPlaylist(t *testing.T) {
|
||||
func TestChannel(t *testing.T) {
|
||||
t.Skip("skip youtube for now")
|
||||
|
||||
ydlResult, ydlResultErr := New(
|
||||
ydlResult, ydlResultErr := NewMetadata(
|
||||
context.Background(),
|
||||
channelRawURL,
|
||||
Options{
|
||||
@@ -150,7 +150,7 @@ func TestUnsupportedURL(t *testing.T) {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
|
||||
_, ydlResultErr := New(context.Background(), "https://www.google.com", Options{})
|
||||
_, ydlResultErr := NewMetadata(context.Background(), "https://www.google.com", Options{})
|
||||
if ydlResultErr == nil {
|
||||
t.Errorf("expected unsupported url")
|
||||
}
|
||||
@@ -165,7 +165,7 @@ func TestPlaylistWithPrivateVideo(t *testing.T) {
|
||||
t.Skip("skip youtube for now")
|
||||
|
||||
plRawURL := "https://www.youtube.com/playlist?list=PLX0g748fkegS54oiDN4AXKl7BR7mLIydP"
|
||||
ydlResult, ydlResultErr := New(context.Background(), plRawURL, Options{
|
||||
ydlResult, ydlResultErr := NewMetadata(context.Background(), plRawURL, Options{
|
||||
Type: TypePlaylist,
|
||||
DownloadThumbnail: false,
|
||||
})
|
||||
@@ -184,7 +184,7 @@ func TestPlaylistWithPrivateVideo(t *testing.T) {
|
||||
func TestSubtitles(t *testing.T) {
|
||||
t.Skip("skip youtube for now")
|
||||
|
||||
ydlResult, ydlResultErr := New(
|
||||
ydlResult, ydlResultErr := NewMetadata(
|
||||
context.Background(),
|
||||
subtitlesTestVideoRawURL,
|
||||
Options{
|
||||
@@ -228,7 +228,7 @@ func TestDownloadSections(t *testing.T) {
|
||||
t.Errorf("failed to check ffmpeg installed: %s", err)
|
||||
}
|
||||
|
||||
ydlResult, ydlResultErr := New(
|
||||
ydlResult, ydlResultErr := NewMetadata(
|
||||
context.Background(),
|
||||
testVideoRawURL,
|
||||
Options{
|
||||
@@ -300,7 +300,7 @@ func TestErrorNotAPlaylist(t *testing.T) {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
|
||||
_, ydlResultErr := New(context.Background(), testVideoRawURL, Options{
|
||||
_, ydlResultErr := NewMetadata(context.Background(), testVideoRawURL, Options{
|
||||
Type: TypePlaylist,
|
||||
DownloadThumbnail: false,
|
||||
})
|
||||
@@ -314,7 +314,7 @@ func TestErrorNotASingleEntry(t *testing.T) {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
|
||||
_, ydlResultErr := New(context.Background(), playlistRawURL, Options{
|
||||
_, ydlResultErr := NewMetadata(context.Background(), playlistRawURL, Options{
|
||||
Type: TypeSingle,
|
||||
DownloadThumbnail: false,
|
||||
})
|
||||
@@ -329,7 +329,7 @@ func TestOptionDownloader(t *testing.T) {
|
||||
t.Skip("skipping test in short mode.")
|
||||
}
|
||||
|
||||
ydlResult, ydlResultErr := New(
|
||||
ydlResult, ydlResultErr := NewMetadata(
|
||||
context.Background(),
|
||||
testVideoRawURL,
|
||||
Options{
|
||||
@@ -356,7 +356,7 @@ func TestOptionDownloader(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInvalidOptionTypeField(t *testing.T) {
|
||||
_, err := New(context.Background(), playlistRawURL, Options{
|
||||
_, err := NewMetadata(context.Background(), playlistRawURL, Options{
|
||||
Type: 42,
|
||||
})
|
||||
if err == nil {
|
||||
@@ -371,7 +371,7 @@ func TestDownloadPlaylistEntry(t *testing.T) {
|
||||
|
||||
// Download file by specifying the playlist index
|
||||
stderrBuf := &bytes.Buffer{}
|
||||
r, err := New(context.Background(), playlistRawURL, Options{
|
||||
r, err := NewMetadata(context.Background(), playlistRawURL, Options{
|
||||
StderrFn: func(cmd *exec.Cmd) io.Writer {
|
||||
return stderrBuf
|
||||
},
|
||||
@@ -425,7 +425,7 @@ func TestDownloadPlaylistEntry(t *testing.T) {
|
||||
// Download the same file but with the direct link
|
||||
url := "https://soundcloud.com/mattheis/b1-mattheis-ben-m"
|
||||
stderrBuf = &bytes.Buffer{}
|
||||
r, err = New(context.Background(), url, Options{
|
||||
r, err = NewMetadata(context.Background(), url, Options{
|
||||
StderrFn: func(cmd *exec.Cmd) io.Writer {
|
||||
return stderrBuf
|
||||
},
|
||||
@@ -478,7 +478,7 @@ func TestDownloadPlaylistEntry(t *testing.T) {
|
||||
func TestFormatDownloadError(t *testing.T) {
|
||||
t.Skip("test URL broken")
|
||||
|
||||
ydl, ydlErr := New(
|
||||
ydl, ydlErr := NewMetadata(
|
||||
context.Background(),
|
||||
"https://www.reddit.com/r/newsbabes/s/92rflI0EB0",
|
||||
Options{},
|
@@ -1,4 +1,4 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -13,7 +13,7 @@ func Download(
|
||||
filter string,
|
||||
) (*DownloadResult, error) {
|
||||
options.noInfoDownload = true
|
||||
d, err := New(ctx, rawURL, options)
|
||||
d, err := NewMetadata(ctx, rawURL, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -15,7 +15,7 @@ func TestDownload(t *testing.T) {
|
||||
}
|
||||
|
||||
stderrBuf := &bytes.Buffer{}
|
||||
r, err := New(context.Background(), testVideoRawURL, Options{
|
||||
r, err := NewMetadata(context.Background(), testVideoRawURL, Options{
|
||||
StderrFn: func(cmd *exec.Cmd) io.Writer {
|
||||
return stderrBuf
|
||||
},
|
@@ -1,4 +1,4 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
import (
|
||||
"errors"
|
@@ -1,4 +1,4 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
import (
|
||||
"fmt"
|
@@ -1,4 +1,4 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -107,7 +107,7 @@ func infoFromURL(
|
||||
) (info Info, rawJSON []byte, err error) {
|
||||
cmd := exec.CommandContext(
|
||||
ctx,
|
||||
FindBin(),
|
||||
FindYtDlpBin(),
|
||||
// see comment below about ignoring errors for playlists
|
||||
"--ignore-errors",
|
||||
// TODO: deprecated in yt-dlp?
|
38
internal/photoprism/dl/metadata.go
Normal file
38
internal/photoprism/dl/metadata.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package dl
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Metadata represents information and options related to a video download URL.
|
||||
type Metadata struct {
|
||||
Info Info
|
||||
RawURL string
|
||||
RawJSON []byte // saved raw JSON. Used later when downloading
|
||||
Options Options // options passed to NewMetadata
|
||||
}
|
||||
|
||||
// NewMetadata downloads metadata for URL
|
||||
func NewMetadata(ctx context.Context, rawURL string, options Options) (result Metadata, err error) {
|
||||
if options.noInfoDownload {
|
||||
return Metadata{
|
||||
RawURL: rawURL,
|
||||
Options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
info, rawJSON, err := infoFromURL(ctx, rawURL, options)
|
||||
if err != nil {
|
||||
return Metadata{}, err
|
||||
}
|
||||
|
||||
rawJSONCopy := make([]byte, len(rawJSON))
|
||||
copy(rawJSONCopy, rawJSON)
|
||||
|
||||
return Metadata{
|
||||
Info: info,
|
||||
RawURL: rawURL,
|
||||
RawJSON: rawJSONCopy,
|
||||
Options: options,
|
||||
}, nil
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Options for New()
|
||||
// Options for NewMetadata()
|
||||
type Options struct {
|
||||
Type Type
|
||||
PlaylistStart uint // --playlist-start
|
||||
@@ -38,7 +38,7 @@ type Options struct {
|
||||
Fixup string // --fixup
|
||||
SortingFormat string // --format-sort
|
||||
|
||||
// Set to true if you don't want to use the result.Info structure after the goutubedl.New() call,
|
||||
// Set to true if you don't want to use the result.Info structure after the goutubedl.NewMetadata() call,
|
||||
// so the given URL will be downloaded in a single pass in the DownloadResult.Download() call.
|
||||
noInfoDownload bool
|
||||
}
|
||||
@@ -52,9 +52,10 @@ type DownloadOptions struct {
|
||||
ForceOverwrites bool // --force-overwrites replaces existing files
|
||||
DisableCaching bool // --no-cache-dir
|
||||
PlaylistIndex int // --playlist-items index of the file to download if there is more than one video
|
||||
Output string
|
||||
}
|
||||
|
||||
func (result Result) DownloadWithOptions(
|
||||
func (result Metadata) DownloadWithOptions(
|
||||
ctx context.Context,
|
||||
options DownloadOptions,
|
||||
) (*DownloadResult, error) {
|
||||
@@ -89,7 +90,7 @@ func (result Result) DownloadWithOptions(
|
||||
|
||||
cmd := exec.CommandContext(
|
||||
ctx,
|
||||
FindBin(),
|
||||
FindYtDlpBin(),
|
||||
// see comment below about ignoring errors for playlists
|
||||
"--ignore-errors",
|
||||
// TODO: deprecated in yt-dlp?
|
||||
@@ -101,10 +102,14 @@ func (result Result) DownloadWithOptions(
|
||||
"--restrict-filenames",
|
||||
// use .netrc authentication data
|
||||
"--netrc",
|
||||
// write to stdout
|
||||
"--output", "-",
|
||||
)
|
||||
|
||||
if options.Output != "" {
|
||||
cmd.Args = append(cmd.Args, "--output", options.Output)
|
||||
} else {
|
||||
cmd.Args = append(cmd.Args, "--output", "-")
|
||||
}
|
||||
|
||||
if result.Options.noInfoDownload {
|
||||
// provide URL via stdin for security, youtube-dl has some run command args
|
||||
cmd.Args = append(cmd.Args, "--batch-file", "-")
|
||||
@@ -131,6 +136,7 @@ func (result Result) DownloadWithOptions(
|
||||
} else {
|
||||
cmd.Args = append(cmd.Args, "--load-info", jsonTempPath)
|
||||
}
|
||||
|
||||
// force IPV4 Usage
|
||||
if result.Options.UseIPV4 {
|
||||
cmd.Args = append(cmd.Args, "-4")
|
@@ -1,18 +1,10 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Result metadata for a URL
|
||||
type Result struct {
|
||||
Info Info
|
||||
RawURL string
|
||||
RawJSON []byte // saved raw JSON. Used later when downloading
|
||||
Options Options // options passed to New
|
||||
}
|
||||
|
||||
// DownloadResult download result
|
||||
type DownloadResult struct {
|
||||
reader io.ReadCloser
|
||||
@@ -22,7 +14,7 @@ type DownloadResult struct {
|
||||
// Download format matched by filter (usually a format id or quality designator).
|
||||
// If filter is empty, then youtube-dl will use its default format selector.
|
||||
// It's a shortcut of DownloadWithOptions where the options use the default value
|
||||
func (result Result) Download(ctx context.Context, filter string) (*DownloadResult, error) {
|
||||
func (result Metadata) Download(ctx context.Context, filter string) (*DownloadResult, error) {
|
||||
return result.DownloadWithOptions(ctx, DownloadOptions{
|
||||
Filter: filter,
|
||||
})
|
||||
@@ -41,7 +33,7 @@ func (dr *DownloadResult) Close() error {
|
||||
|
||||
// Formats return all formats
|
||||
// helper to take care of mixed info and format
|
||||
func (result Result) Formats() []Format {
|
||||
func (result Metadata) Formats() []Format {
|
||||
if len(result.Info.Formats) > 0 {
|
||||
return result.Info.Formats
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
// Subtitle youtube-dl subtitle
|
||||
type Subtitle struct {
|
@@ -1,4 +1,4 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
type Thumbnail struct {
|
||||
ID string `json:"id"`
|
@@ -1,4 +1,4 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
// Type of response you want
|
||||
type Type int
|
@@ -1,4 +1,4 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
// Version of youtube-dl.
|
||||
// Might be a good idea to call at start to assert that youtube-dl can be found.
|
||||
func Version(ctx context.Context) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, FindBin(), "--version")
|
||||
cmd := exec.CommandContext(ctx, FindYtDlpBin(), "--version")
|
||||
versionBytes, cmdErr := cmd.Output()
|
||||
if cmdErr != nil {
|
||||
return "", cmdErr
|
@@ -1,4 +1,4 @@
|
||||
package ytdl
|
||||
package dl
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -21,8 +21,8 @@ func TestVersion(t *testing.T) {
|
||||
}
|
||||
})
|
||||
t.Run("InvalidBin", func(t *testing.T) {
|
||||
defer func(orig string) { Bin = orig }(Bin)
|
||||
Bin = "/non-existing"
|
||||
defer func(orig string) { YtDlpBin = orig }(YtDlpBin)
|
||||
YtDlpBin = "/non-existing"
|
||||
|
||||
_, versionErr := Version(context.Background())
|
||||
if versionErr == nil || !strings.Contains(versionErr.Error(), "no such file or directory") {
|
@@ -532,6 +532,10 @@ func (m *MediaFile) MimeType() string {
|
||||
|
||||
m.mimeType = fs.MimeType(fileName)
|
||||
|
||||
if m.mimeType == header.ContentTypeMp4 && m.MetaData().Codec == video.CodecM2TS {
|
||||
m.mimeType = header.ContentTypeM2TS
|
||||
}
|
||||
|
||||
return m.mimeType
|
||||
}
|
||||
|
||||
@@ -814,6 +818,17 @@ func (m *MediaFile) IsAvifS() bool {
|
||||
return m.HasFileType(fs.ImageAvifS)
|
||||
}
|
||||
|
||||
// IsM2TS checks if the file is an MPEG-2 Transport Stream (M2TS) container.
|
||||
func (m *MediaFile) IsM2TS() bool {
|
||||
if t := fs.FileType(m.fileName); t == fs.VideoM2TS {
|
||||
return true
|
||||
} else if t == fs.VideoMp4 || t == fs.VideoAvcHD {
|
||||
return m.HasMimeType(header.ContentTypeM2TS)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsBmp checks if the file is a bitmap image with a supported file type extension.
|
||||
func (m *MediaFile) IsBmp() bool {
|
||||
if fs.FileType(m.fileName) != fs.ImageBmp {
|
||||
|
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/meta"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/media/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/media/projection"
|
||||
"github.com/photoprism/photoprism/pkg/media/video"
|
||||
"github.com/photoprism/photoprism/pkg/time/tz"
|
||||
@@ -73,10 +74,11 @@ func TestMediaFile_SidecarJsonName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMediaFile_NeedsExifToolJson(t *testing.T) {
|
||||
c := config.TestConfig()
|
||||
|
||||
t.Run("false", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/beach_sand.jpg")
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/beach_sand.jpg")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -85,9 +87,7 @@ func TestMediaFile_NeedsExifToolJson(t *testing.T) {
|
||||
assert.True(t, mediaFile.NeedsExifToolJson())
|
||||
})
|
||||
t.Run("true", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/blue-go-video.mp4")
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/blue-go-video.mp4")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -96,9 +96,7 @@ func TestMediaFile_NeedsExifToolJson(t *testing.T) {
|
||||
assert.True(t, mediaFile.NeedsExifToolJson())
|
||||
})
|
||||
t.Run("true", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/blue-go-video.mp4.json")
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/blue-go-video.mp4.json")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -109,10 +107,108 @@ func TestMediaFile_NeedsExifToolJson(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMediaFile_CreateExifToolJson(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
c := config.TestConfig()
|
||||
|
||||
t.Run("bear.m2ts", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(filepath.Join(c.ExamplesPath(), "bear.m2ts"))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, fs.VideoM2TS, mediaFile.FileType())
|
||||
|
||||
jsonName, err := mediaFile.ExifToolJsonName()
|
||||
|
||||
if fs.FileExists(jsonName) {
|
||||
if err = os.Remove(jsonName); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, mediaFile.NeedsExifToolJson())
|
||||
|
||||
err = mediaFile.CreateExifToolJson(NewConvert(c))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data := mediaFile.MetaData()
|
||||
|
||||
assert.Empty(t, err)
|
||||
|
||||
assert.IsType(t, meta.Data{}, data)
|
||||
|
||||
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, video.CodecM2TS, data.Codec)
|
||||
assert.Equal(t, 320, data.Width)
|
||||
assert.Equal(t, 192, data.Height)
|
||||
assert.Equal(t, false, data.Flash)
|
||||
assert.Equal(t, "", data.Caption)
|
||||
assert.True(t, mediaFile.IsM2TS())
|
||||
assert.True(t, mediaFile.IsVideo())
|
||||
assert.True(t, mediaFile.HasMediaType(media.Video))
|
||||
assert.Equal(t, fs.VideoM2TS, mediaFile.FileType())
|
||||
assert.Equal(t, header.ContentTypeM2TS, mediaFile.MimeType())
|
||||
assert.Equal(t, header.ContentTypeM2TS, mediaFile.ContentType())
|
||||
|
||||
if err = os.Remove(jsonName); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
t.Run("m2ts.mp4", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(filepath.Join(c.ExamplesPath(), "m2ts.mp4"))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, fs.VideoMp4, mediaFile.FileType())
|
||||
|
||||
jsonName, err := mediaFile.ExifToolJsonName()
|
||||
|
||||
if fs.FileExists(jsonName) {
|
||||
if err = os.Remove(jsonName); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, mediaFile.NeedsExifToolJson())
|
||||
|
||||
err = mediaFile.CreateExifToolJson(NewConvert(c))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data := mediaFile.MetaData()
|
||||
|
||||
assert.Empty(t, err)
|
||||
|
||||
assert.IsType(t, meta.Data{}, data)
|
||||
|
||||
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, video.CodecM2TS, data.Codec)
|
||||
assert.Equal(t, 320, data.Width)
|
||||
assert.Equal(t, 192, data.Height)
|
||||
assert.Equal(t, false, data.Flash)
|
||||
assert.Equal(t, "", data.Caption)
|
||||
assert.True(t, mediaFile.IsM2TS())
|
||||
assert.True(t, mediaFile.IsVideo())
|
||||
assert.True(t, mediaFile.HasMediaType(media.Video))
|
||||
assert.Equal(t, fs.VideoMp4, mediaFile.FileType())
|
||||
assert.Equal(t, header.ContentTypeM2TS, mediaFile.MimeType())
|
||||
assert.Equal(t, header.ContentTypeM2TS, mediaFile.ContentType())
|
||||
|
||||
if err = os.Remove(jsonName); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
t.Run("gopher-video.mp4", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/gopher-video.mp4")
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/gopher-video.mp4")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -128,7 +224,7 @@ func TestMediaFile_CreateExifToolJson(t *testing.T) {
|
||||
|
||||
assert.True(t, mediaFile.NeedsExifToolJson())
|
||||
|
||||
err = mediaFile.CreateExifToolJson(NewConvert(conf))
|
||||
err = mediaFile.CreateExifToolJson(NewConvert(c))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -156,10 +252,10 @@ func TestMediaFile_CreateExifToolJson(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMediaFile_Exif_Jpeg(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
c := config.TestConfig()
|
||||
|
||||
t.Run("elephants.jpg", func(t *testing.T) {
|
||||
img, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
|
||||
img, err := NewMediaFile(c.ExamplesPath() + "/elephants.jpg")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -198,7 +294,7 @@ func TestMediaFile_Exif_Jpeg(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("fern_green.jpg", func(t *testing.T) {
|
||||
img, err := NewMediaFile(conf.ExamplesPath() + "/fern_green.jpg")
|
||||
img, err := NewMediaFile(c.ExamplesPath() + "/fern_green.jpg")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -232,9 +328,8 @@ func TestMediaFile_Exif_Jpeg(t *testing.T) {
|
||||
t.Logf("UTC: %s", info.TakenAt.String())
|
||||
t.Logf("Local: %s", info.TakenAtLocal.String())
|
||||
})
|
||||
|
||||
t.Run("blue-go-video.mp4", func(t *testing.T) {
|
||||
img, err := NewMediaFile(conf.ExamplesPath() + "/blue-go-video.mp4")
|
||||
img, err := NewMediaFile(c.ExamplesPath() + "/blue-go-video.mp4")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -246,7 +341,6 @@ func TestMediaFile_Exif_Jpeg(t *testing.T) {
|
||||
|
||||
assert.IsType(t, meta.Data{}, info)
|
||||
})
|
||||
|
||||
t.Run("panorama360.jpg", func(t *testing.T) {
|
||||
img, err := NewMediaFile("testdata/panorama360.jpg")
|
||||
|
||||
@@ -281,7 +375,6 @@ func TestMediaFile_Exif_Jpeg(t *testing.T) {
|
||||
assert.Equal(t, 1, data.Orientation)
|
||||
assert.Equal(t, projection.Equirectangular.String(), data.Projection)
|
||||
})
|
||||
|
||||
t.Run("digikam.jpg", func(t *testing.T) {
|
||||
img, err := NewMediaFile("testdata/digikam.jpg")
|
||||
|
||||
|
@@ -765,6 +765,26 @@ func TestMediaFile_MimeType(t *testing.T) {
|
||||
assert.Equal(t, "video/mp4", f.MimeType())
|
||||
}
|
||||
})
|
||||
t.Run("bear.m2ts", func(t *testing.T) {
|
||||
if f, err := NewMediaFile(filepath.Join(c.ExamplesPath(), "bear.m2ts")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.True(t, f.IsM2TS())
|
||||
assert.Equal(t, fs.VideoM2TS, f.FileType())
|
||||
assert.Equal(t, header.ContentTypeM2TS, f.MimeType())
|
||||
assert.Equal(t, header.ContentTypeM2TS, f.ContentType())
|
||||
}
|
||||
})
|
||||
t.Run("m2ts.mp4", func(t *testing.T) {
|
||||
if f, err := NewMediaFile(filepath.Join(c.ExamplesPath(), "m2ts.mp4")); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.False(t, f.IsM2TS())
|
||||
assert.Equal(t, fs.VideoMp4, f.FileType())
|
||||
assert.Equal(t, header.ContentTypeMp4, f.MimeType())
|
||||
assert.Equal(t, header.ContentTypeMp4, f.ContentType())
|
||||
}
|
||||
})
|
||||
t.Run("earth.avi", func(t *testing.T) {
|
||||
if f, err := NewMediaFile(filepath.Join(c.ExamplesPath(), "earth.avi")); err != nil {
|
||||
t.Fatal(err)
|
||||
|
@@ -1,30 +0,0 @@
|
||||
package ytdl
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// New downloads metadata for URL
|
||||
func New(ctx context.Context, rawURL string, options Options) (result Result, err error) {
|
||||
if options.noInfoDownload {
|
||||
return Result{
|
||||
RawURL: rawURL,
|
||||
Options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
info, rawJSON, err := infoFromURL(ctx, rawURL, options)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
|
||||
rawJSONCopy := make([]byte, len(rawJSON))
|
||||
copy(rawJSONCopy, rawJSON)
|
||||
|
||||
return Result{
|
||||
Info: info,
|
||||
RawURL: rawURL,
|
||||
RawJSON: rawJSONCopy,
|
||||
Options: options,
|
||||
}, nil
|
||||
}
|
@@ -155,8 +155,11 @@ var Extensions = FileExtensions{
|
||||
".flv": VideoFlash,
|
||||
".f4v": VideoFlash,
|
||||
".mkv": VideoMkv,
|
||||
".ts": VideoM2TS,
|
||||
".m2t": VideoM2TS,
|
||||
".m2ts": VideoM2TS,
|
||||
".mp2t": VideoM2TS,
|
||||
".mts": VideoAvcHD,
|
||||
".m2ts": VideoBDAV,
|
||||
".ogv": VideoTheora,
|
||||
".ogg": VideoTheora,
|
||||
".ogx": VideoTheora,
|
||||
|
@@ -51,8 +51,8 @@ var TypeInfo = TypeMap{
|
||||
VideoMkv: "Matroska Multimedia Container",
|
||||
VideoMpeg: "Moving Picture Experts Group (MPEG)",
|
||||
VideoMjpeg: "Motion JPEG",
|
||||
VideoM2TS: "MPEG-2 Transport Stream (M2TS)",
|
||||
VideoAvcHD: "Advanced Video Coding High Definition (AVCHD)",
|
||||
VideoBDAV: "Blu-ray MPEG-2 Transport Stream",
|
||||
VideoTheora: "Ogg Media (OGG)",
|
||||
SidecarXMP: "Adobe Extensible Metadata Platform",
|
||||
SidecarAppleXml: "Apple Image Edits XML",
|
||||
|
@@ -87,8 +87,8 @@ const (
|
||||
Video3GP Type = "3gp" // Mobile Multimedia Container, MPEG-4 Part 12
|
||||
Video3G2 Type = "3g2" // Similar to 3GP, consumes less space & bandwidth
|
||||
VideoFlash Type = "flv" // Flash Video
|
||||
VideoM2TS Type = "m2t" // MPEG-2 Transport Stream (M2TS)
|
||||
VideoAvcHD Type = "mts" // AVCHD (Advanced Video Coding High Definition)
|
||||
VideoBDAV Type = "m2ts" // Blu-ray MPEG-2 Transport Stream
|
||||
VideoTheora Type = "ogv" // Ogg container format maintained by the Xiph.Org, free and open
|
||||
VideoASF Type = "asf" // Advanced Systems/Streaming Format (ASF)
|
||||
VideoAVI Type = "avi" // Microsoft Audio Video Interleave (AVI)
|
||||
|
@@ -29,6 +29,9 @@ func MimeType(filename string) (mimeType string) {
|
||||
// Determine mime type based on the extension for the following
|
||||
// formats, which otherwise cannot be reliably distinguished:
|
||||
switch fileType {
|
||||
// MPEG-2 Transport Stream
|
||||
case VideoM2TS, VideoAvcHD:
|
||||
return header.ContentTypeM2TS
|
||||
// Apple QuickTime Container
|
||||
case VideoMov:
|
||||
return header.ContentTypeMov
|
||||
|
@@ -56,8 +56,8 @@ var Formats = map[fs.Type]Type{
|
||||
fs.Video3GP: Video,
|
||||
fs.Video3G2: Video,
|
||||
fs.VideoFlash: Video,
|
||||
fs.VideoM2TS: Video,
|
||||
fs.VideoAvcHD: Video,
|
||||
fs.VideoBDAV: Video,
|
||||
fs.VideoTheora: Video,
|
||||
fs.VideoASF: Video,
|
||||
fs.VideoWMV: Video,
|
||||
|
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
// Standard ContentType strings for audio and video files:
|
||||
const (
|
||||
ContentTypeM2TS = "video/mp2t"
|
||||
ContentTypeM4v = "video/x-m4v"
|
||||
ContentTypeMp4 = "video/mp4"
|
||||
ContentTypeMp4Avc = ContentTypeMp4 + "; codecs=\"avc1\"" // MPEG-4 AVC (H.264)
|
||||
|
@@ -76,7 +76,6 @@ var CompatibleBrands = Chunks{
|
||||
ChunkHEV2,
|
||||
ChunkHEV3,
|
||||
ChunkDVHE,
|
||||
ChunkHEIC,
|
||||
ChunkAV01,
|
||||
ChunkAV1C,
|
||||
ChunkMMP4,
|
||||
|
@@ -60,13 +60,13 @@ func (c Chunk) FileOffset(fileName string) (int, error) {
|
||||
|
||||
defer file.Close()
|
||||
|
||||
index, err := c.DataOffset(file, -1)
|
||||
index, err := c.DataOffset(file, 0, -1)
|
||||
|
||||
return index, err
|
||||
}
|
||||
|
||||
// DataOffset returns the index of the chunk in file, or -1 if it was not found.
|
||||
func (c Chunk) DataOffset(file io.ReadSeeker, maxOffset int) (int, error) {
|
||||
func (c Chunk) DataOffset(file io.ReadSeeker, offset, maxOffset int) (int, error) {
|
||||
if file == nil {
|
||||
return -1, errors.New("file is nil")
|
||||
}
|
||||
@@ -79,8 +79,11 @@ func (c Chunk) DataOffset(file io.ReadSeeker, maxOffset int) (int, error) {
|
||||
// Create buffered read seeker.
|
||||
r := bufseekio.NewReadSeeker(file, blockSize, dataSize)
|
||||
|
||||
// Index offset.
|
||||
var offset int
|
||||
if seekOffset, seekErr := r.Seek(int64(offset), io.SeekStart); seekErr != nil {
|
||||
return -1, seekErr
|
||||
} else {
|
||||
offset = int(seekOffset)
|
||||
}
|
||||
|
||||
// Search in batches.
|
||||
for {
|
||||
|
@@ -39,6 +39,17 @@ func TestChunk_FileOffset(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 23213, index)
|
||||
})
|
||||
t.Run("motion-photo.heif", func(t *testing.T) {
|
||||
index, err := ChunkFTYP.FileOffset("testdata/motion-photo.heif")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 4, index)
|
||||
index, err = ChunkHEIC.FileOffset("testdata/motion-photo.heif")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 8, index)
|
||||
index, err = ChunkHVC1.FileOffset("testdata/motion-photo.heif")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 976016, index)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChunks(t *testing.T) {
|
||||
|
@@ -33,6 +33,7 @@ const (
|
||||
CodecVp08 Codec = "vp08" // Google VP8
|
||||
CodecVp09 Codec = "vp09" // Google VP9
|
||||
CodecTheora Codec = "ogv" // Ogg Vorbis Video
|
||||
CodecM2TS Codec = "m2t" // MPEG-2 Transport Stream
|
||||
CodecWebm Codec = "webm" // Google WebM
|
||||
)
|
||||
|
||||
|
@@ -64,6 +64,8 @@ func ContentType(mediaType, fileType, videoCodec string, hdr bool) string {
|
||||
mediaType = header.ContentTypeMp4
|
||||
case fs.VideoMkv.Equal(fileType):
|
||||
mediaType = header.ContentTypeMkv
|
||||
case fs.VideoM2TS.Equal(fileType) || videoCodec == CodecM2TS:
|
||||
mediaType = header.ContentTypeM2TS
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -153,8 +153,7 @@ func Probe(file io.ReadSeeker) (info Info, err error) {
|
||||
// If no AVC video was found, search the video data for High Efficiency Video Coding (HEVC) chunks,
|
||||
// see https://stackoverflow.com/questions/63468587/what-hevc-codec-tag-to-use-with-fmp4-hvc1-or-hev1.
|
||||
if info.VideoCodec == "" {
|
||||
// To improve performance, only search for "hvc1" as that is the most common HEVC video identifier.
|
||||
if fileOffset, fileErr := ChunkHVC1.DataOffset(file, -1); fileOffset > 0 && fileErr == nil {
|
||||
if fileOffset, fileErr := ChunkHVC1.DataOffset(file, 0, -1); fileOffset > 0 && fileErr == nil {
|
||||
info.VideoCodec = CodecHvc1
|
||||
}
|
||||
}
|
||||
|
@@ -252,4 +252,34 @@ func TestProbe(t *testing.T) {
|
||||
assert.Equal(t, false, info.FastStart)
|
||||
assert.Equal(t, true, info.Compatible)
|
||||
})
|
||||
t.Run("motion-photo.heif", func(t *testing.T) {
|
||||
f, fileErr := os.Open("testdata/motion-photo.heif")
|
||||
require.NoError(t, fileErr)
|
||||
defer f.Close()
|
||||
|
||||
info, err := Probe(f)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
|
||||
assert.Equal(t, "", info.FileName)
|
||||
assert.Equal(t, int64(-1), info.FileSize)
|
||||
assert.Equal(t, fs.TypeUnknown, info.FileType)
|
||||
assert.Equal(t, Mp4, info.VideoType)
|
||||
assert.Equal(t, int64(978741), info.VideoOffset)
|
||||
assert.Equal(t, int64(0), info.ThumbOffset)
|
||||
assert.Equal(t, media.Live, info.MediaType)
|
||||
assert.Equal(t, CodecHvc1, info.VideoCodec)
|
||||
assert.Equal(t, header.ContentTypeMp4, info.VideoMimeType)
|
||||
assert.Equal(t, header.ContentTypeMp4HvcMain10, info.VideoContentType())
|
||||
assert.Equal(t, "2.9686s", info.Duration.String())
|
||||
assert.InEpsilon(t, 2.9686, info.Duration.Seconds(), 0.01)
|
||||
assert.Equal(t, 2, info.Tracks)
|
||||
assert.Equal(t, 0, info.VideoWidth)
|
||||
assert.Equal(t, 0, info.VideoHeight)
|
||||
assert.Equal(t, 89, info.Frames)
|
||||
assert.Equal(t, 30.0, info.FPS)
|
||||
assert.Equal(t, false, info.Encrypted)
|
||||
assert.Equal(t, false, info.FastStart)
|
||||
assert.Equal(t, true, info.Compatible)
|
||||
})
|
||||
}
|
||||
|
@@ -74,6 +74,9 @@ var Types = Standards{
|
||||
"mkv1": MkvAv1,
|
||||
"ogg": Theora, // ↓ Theora video in OGG container
|
||||
"ogv": Theora,
|
||||
"m2t": M2TS, // ↓ MPEG-2 Transport Stream container
|
||||
"m2ts": M2TS,
|
||||
"mp2t": M2TS,
|
||||
"mp4": Mp4, // ↓ Unknown codec in MP4 container
|
||||
"mpeg4": Mp4,
|
||||
"webm": Webm, // ↓ Unknown codec in WebM container
|
||||
|
BIN
pkg/media/video/testdata/bear.m2ts
vendored
Normal file
BIN
pkg/media/video/testdata/bear.m2ts
vendored
Normal file
Binary file not shown.
BIN
pkg/media/video/testdata/motion-photo.heif
vendored
Normal file
BIN
pkg/media/video/testdata/motion-photo.heif
vendored
Normal file
Binary file not shown.
@@ -21,6 +21,16 @@ var Mp4 = Type{
|
||||
Public: true,
|
||||
}
|
||||
|
||||
// M2TS specifies the MPEG-2 Transport Stream (M2TS) multimedia container format.
|
||||
var M2TS = Type{
|
||||
Codec: CodecAvc1,
|
||||
FileType: fs.VideoM2TS,
|
||||
ContentType: header.ContentTypeM2TS,
|
||||
WidthLimit: 8192,
|
||||
HeightLimit: 4320,
|
||||
Public: false,
|
||||
}
|
||||
|
||||
// Mov specifies the Apple QuickTime (QT) container format.
|
||||
var Mov = Type{
|
||||
Codec: CodecAvc1,
|
||||
|
Reference in New Issue
Block a user