first working version!
This commit is contained in:
parent
1bba7ef03d
commit
84de96df31
9 changed files with 689 additions and 80 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
|
.DS_Store
|
||||||
config.toml
|
config.toml
|
||||||
|
|
|
||||||
13
go.mod
13
go.mod
|
|
@ -2,10 +2,17 @@ module arimelody.space/live-vod-uploader
|
||||||
|
|
||||||
go 1.25.3
|
go 1.25.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
|
golang.org/x/oauth2 v0.32.0
|
||||||
|
google.golang.org/api v0.254.0
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/auth v0.17.0 // indirect
|
cloud.google.com/go/auth v0.17.0 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
|
github.com/aws/aws-sdk-go v1.38.20 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
|
@ -13,7 +20,9 @@ require (
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
|
github.com/u2takey/ffmpeg-go v0.5.0 // indirect
|
||||||
|
github.com/u2takey/go-utils v0.3.1 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||||
|
|
@ -21,10 +30,8 @@ require (
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||||
golang.org/x/crypto v0.43.0 // indirect
|
golang.org/x/crypto v0.43.0 // indirect
|
||||||
golang.org/x/net v0.46.0 // indirect
|
golang.org/x/net v0.46.0 // indirect
|
||||||
golang.org/x/oauth2 v0.32.0 // indirect
|
|
||||||
golang.org/x/sys v0.37.0 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
golang.org/x/text v0.30.0 // indirect
|
golang.org/x/text v0.30.0 // indirect
|
||||||
google.golang.org/api v0.254.0 // indirect
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||||
google.golang.org/grpc v1.76.0 // indirect
|
google.golang.org/grpc v1.76.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
|
|
|
||||||
77
go.sum
77
go.sum
|
|
@ -4,23 +4,65 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||||
|
github.com/aws/aws-sdk-go v1.38.20 h1:QbzNx/tdfATbdKfubBpkt84OM6oBkxQZRw6+bW2GyeA=
|
||||||
|
github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
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/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/u2takey/ffmpeg-go v0.5.0 h1:r7d86XuL7uLWJ5mzSeQ03uvjfIhiJYvsRAJFCW4uklU=
|
||||||
|
github.com/u2takey/ffmpeg-go v0.5.0/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc=
|
||||||
|
github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys=
|
||||||
|
github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||||
|
|
@ -29,23 +71,58 @@ go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
|
gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/api v0.254.0 h1:jl3XrGj7lRjnlUvZAbAdhINTLbsg5dbjmR90+pTQvt4=
|
google.golang.org/api v0.254.0 h1:jl3XrGj7lRjnlUvZAbAdhINTLbsg5dbjmR90+pTQvt4=
|
||||||
google.golang.org/api v0.254.0/go.mod h1:5BkSURm3D9kAqjGvBNgf0EcbX6Rnrf6UArKkwBzAyqQ=
|
google.golang.org/api v0.254.0/go.mod h1:5BkSURm3D9kAqjGvBNgf0EcbX6Rnrf6UArKkwBzAyqQ=
|
||||||
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||||
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||||
|
|
|
||||||
255
main.go
255
main.go
|
|
@ -4,11 +4,19 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
toml "github.com/pelletier/go-toml/v2"
|
toml "github.com/pelletier/go-toml/v2"
|
||||||
"google.golang.org/api/option"
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/google"
|
||||||
"google.golang.org/api/youtube/v3"
|
"google.golang.org/api/youtube/v3"
|
||||||
|
|
||||||
|
"arimelody.space/live-vod-uploader/scanner"
|
||||||
|
vid "arimelody.space/live-vod-uploader/video"
|
||||||
|
yt "arimelody.space/live-vod-uploader/youtube"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|
@ -23,99 +31,210 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var DEFAULT_TAGS = []string{
|
const CONFIG_FILENAME = "config.toml"
|
||||||
"ari melody",
|
|
||||||
"ari melody LIVE",
|
func showHelp() {
|
||||||
"livestream",
|
execSplits := strings.Split(os.Args[0], "/")
|
||||||
"vtuber",
|
execName := execSplits[len(execSplits) - 1]
|
||||||
"twitch",
|
fmt.Printf(
|
||||||
"gaming",
|
"usage: %s [options] [directory]\n\n" +
|
||||||
"let's play",
|
"options:\n" +
|
||||||
"full VOD",
|
"\t-h, --help: Show this help message.\n" +
|
||||||
"VOD",
|
"\t-v, --verbose: Show verbose logging output.\n" +
|
||||||
"stream",
|
"\t--init: Initialise `directory` as a VOD directory.\n",
|
||||||
"archive",
|
execName)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
CATEGORY_GAMING = "20"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 || os.Args[1] == "--help" || os.Args[1] == "-h" {
|
||||||
fmt.Printf("usage: %s <video ID>\n", os.Args[0])
|
showHelp()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// videoID := os.Args[1]
|
var directory string
|
||||||
|
var initDirectory bool = false
|
||||||
|
var verbose bool = false
|
||||||
|
|
||||||
cfgBytes, err := os.ReadFile("config.toml")
|
for i, arg := range os.Args {
|
||||||
|
if i == 0 { continue }
|
||||||
|
if strings.HasPrefix(arg, "-") {
|
||||||
|
switch arg {
|
||||||
|
|
||||||
|
case "-h":
|
||||||
|
fallthrough
|
||||||
|
case "--help":
|
||||||
|
showHelp()
|
||||||
|
os.Exit(0)
|
||||||
|
|
||||||
|
case "--init":
|
||||||
|
initDirectory = true
|
||||||
|
|
||||||
|
case "-v":
|
||||||
|
fallthrough
|
||||||
|
case "--verbose":
|
||||||
|
verbose = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "Unknown option `%s`\n", arg)
|
||||||
|
os.Exit(1)
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
directory = arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := Config{}
|
||||||
|
cfgBytes, err := os.ReadFile(CONFIG_FILENAME)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "fatal: failed to read config file: %s\n", err.Error())
|
log.Fatalf("Failed to read config file: %v", err)
|
||||||
|
|
||||||
|
tomlBytes, err := toml.Marshal(&cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to marshal json: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
cfg := Config{}
|
|
||||||
|
err = os.WriteFile(CONFIG_FILENAME, tomlBytes, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to write config file: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("New config file created. Please edit this before running again!")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
err = toml.Unmarshal(cfgBytes, &cfg)
|
err = toml.Unmarshal(cfgBytes, &cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "fatal: failed to parse config: %s\n", err.Error())
|
log.Fatalf("Failed to parse config: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
if initDirectory {
|
||||||
service, err := youtube.NewService(
|
dirInfo, err := os.Stat(directory)
|
||||||
ctx,
|
if err != nil {
|
||||||
option.WithScopes(youtube.YoutubeUploadScope),
|
if err == os.ErrNotExist {
|
||||||
option.WithAPIKey(cfg.Google.ApiKey),
|
log.Fatalf("No such directory: %s", directory)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
log.Fatalf("Failed to open directory: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if !dirInfo.IsDir() {
|
||||||
|
log.Fatalf("Not a directory: %s", directory)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
dirEntry, err := os.ReadDir(directory)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to open directory: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
for _, entry := range dirEntry {
|
||||||
|
if !entry.IsDir() && entry.Name() == "metadata.toml" {
|
||||||
|
log.Printf("Directory `%s` already initialised", directory)
|
||||||
|
os.Exit(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMetadata := scanner.DefaultMetadata()
|
||||||
|
metadataStr, _ := toml.Marshal(defaultMetadata)
|
||||||
|
err = os.WriteFile(path.Join(directory, "metadata.toml"), metadataStr, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to write to file: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
log.Printf("Directory successfully initialised")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata, err := scanner.FetchMetadata(directory)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to fetch VOD metadata: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if metadata == nil {
|
||||||
|
log.Fatal("Directory contained no metadata. Use `--init` to initialise this directory.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
vodFiles, err := scanner.FetchVideos(metadata.FootageDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to fetch VOD filenames: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if len(vodFiles) == 0 {
|
||||||
|
log.Fatal("Directory contained no VOD files (expecting .mkv)")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", "\t")
|
||||||
|
fmt.Printf("Directory metadata: ")
|
||||||
|
enc.Encode(metadata)
|
||||||
|
fmt.Printf("\nVOD files available: ")
|
||||||
|
enc.Encode(vodFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
video, err := yt.BuildVideo(metadata)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to build video template: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if verbose {
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
fmt.Printf("\nVideo template: ")
|
||||||
|
enc.Encode(video)
|
||||||
|
|
||||||
|
title, err := yt.BuildTitle(video)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to build video title: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
description, err := yt.BuildDescription(video)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to build video description: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf(
|
||||||
|
"\nTITLE: %s\nDESCRIPTION: %s",
|
||||||
|
title, description,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = vid.ConcatVideo(video, vodFiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "fatal: failed to create youtube service: %s\n", err.Error())
|
log.Fatalf("Failed to concatenate VOD files: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
videoService := youtube.NewVideosService(service)
|
// okay actual youtube stuff now
|
||||||
|
|
||||||
// get video by ID
|
// TODO: tidy up oauth flow with localhost webserver
|
||||||
{
|
ctx := context.Background()
|
||||||
// call := service.Videos.List([]string{
|
config := &oauth2.Config{
|
||||||
// "snippet", "contentDetails", "statistics", "status",
|
ClientID: cfg.Google.ClientID,
|
||||||
// }).Id(videoID)
|
ClientSecret: cfg.Google.ClientSecret,
|
||||||
// res, err := call.Do()
|
Endpoint: google.Endpoint,
|
||||||
// if err != nil {
|
Scopes: []string{ youtube.YoutubeScope },
|
||||||
// fmt.Fprintf(os.Stderr, "fatal: failed to request videos list: %s\n", err.Error())
|
RedirectURL: "http://localhost:8090",
|
||||||
// os.Exit(1)
|
}
|
||||||
// }
|
verifier := oauth2.GenerateVerifier()
|
||||||
|
url := config.AuthCodeURL("state", oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(verifier))
|
||||||
|
log.Printf("Visit URL to initiate OAuth2: %s", url)
|
||||||
|
|
||||||
// data, err := json.MarshalIndent(res, "", " ")
|
var code string
|
||||||
// if err != nil {
|
fmt.Print("Enter OAuth2 code: ")
|
||||||
// fmt.Fprintf(os.Stderr, "fatal: failed to marshal json: %s\n", err.Error())
|
if _, err := fmt.Scan(&code); err != nil {
|
||||||
// os.Exit(1)
|
log.Fatalf("Failed to read oauth2 code: %v", err)
|
||||||
// }
|
|
||||||
|
|
||||||
// fmt.Println(string(data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
call := videoService.Insert([]string{
|
token, err := config.Exchange(ctx, code, oauth2.VerifierOption(verifier))
|
||||||
"snippet", "status",
|
log.Printf("Token expires on %s\n", token.Expiry.Format("02 Jan 2006"))
|
||||||
}, &youtube.Video{
|
|
||||||
Snippet: &youtube.VideoSnippet{
|
|
||||||
Title: "Untitled Video",
|
|
||||||
Description: "No description",
|
|
||||||
Tags: DEFAULT_TAGS,
|
|
||||||
CategoryId: CATEGORY_GAMING, // gaming
|
|
||||||
},
|
|
||||||
}).NotifySubscribers(false)
|
|
||||||
// TODO: call.Media()
|
|
||||||
video, err := call.Do()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "fatal: failed to upload video: %s\n", err.Error())
|
log.Fatalf("Could not exchange OAuth2 code: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.MarshalIndent(video, "", " ")
|
yt.UploadVideo(ctx, token, video)
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "fatal: failed to marshal video data json: %s\n", err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(string(data))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
90
scanner/scanner.go
Normal file
90
scanner/scanner.go
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
package scanner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Category struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Url string
|
||||||
|
}
|
||||||
|
|
||||||
|
Metadata struct {
|
||||||
|
Title string
|
||||||
|
Date string
|
||||||
|
Part int
|
||||||
|
FootageDir string
|
||||||
|
Category *Category
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func FetchVideos(directory string) ([]string, error) {
|
||||||
|
entries, err := os.ReadDir(directory)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
files := []string{}
|
||||||
|
|
||||||
|
for _, item := range entries {
|
||||||
|
if item.IsDir() { continue }
|
||||||
|
if !strings.HasSuffix(item.Name(), ".mkv") { continue }
|
||||||
|
files = append(files, item.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchMetadata(directory string) (*Metadata, error) {
|
||||||
|
entries, err := os.ReadDir(directory)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range entries {
|
||||||
|
if item.IsDir() { continue }
|
||||||
|
if item.Name() == "metadata.toml" {
|
||||||
|
metadata, err := ParseMetadata(filepath.Join(directory, item.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
metadata.FootageDir = filepath.Join(directory, metadata.FootageDir)
|
||||||
|
return metadata, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMetadata(filename string) (*Metadata, error) {
|
||||||
|
metadata := &Metadata{}
|
||||||
|
file, err := os.OpenFile(filename, os.O_RDONLY, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = toml.NewDecoder(file).Decode(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultMetadata() Metadata {
|
||||||
|
return Metadata{
|
||||||
|
Title: "Untitled Stream",
|
||||||
|
Date: time.Now().Format("2006-01-02"),
|
||||||
|
Part: 0,
|
||||||
|
Category: &Category{
|
||||||
|
Name: "Something",
|
||||||
|
Type: "",
|
||||||
|
Url: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
17
template/description.txt
Normal file
17
template/description.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
streamed on {{.Date}}
|
||||||
|
💚 watch ari melody LIVE: https://twitch.tv/arispacegirl
|
||||||
|
{{if .Title}}{{if eq .Title.Type "game"}}
|
||||||
|
🎮 play {{.Title.Name}}:
|
||||||
|
{{.Title.Url}}
|
||||||
|
{{else}}
|
||||||
|
✨ check out {{.Title.Name}}:
|
||||||
|
{{.Title.Url}}
|
||||||
|
{{end}}{{end}}
|
||||||
|
💫 ari's place: https://arimelody.space
|
||||||
|
💬 ari melody discord: https://arimelody.space/discord
|
||||||
|
|
||||||
|
🎵 intro music:
|
||||||
|
Mameyudoufu - Second Brain
|
||||||
|
https://www.youtube.com/watch?v=5leDpfJLzLU&list=OLAK5uy_nuYS1Q3Bj8DkuJq-ylYmBZHetepavg0lI
|
||||||
|
|
||||||
|
🥰 i hope you're having a lovely day!
|
||||||
1
template/title.txt
Normal file
1
template/title.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{{.Title.Name}}{{if gt .Part 0}}, part {{.Part}}{{end}} | ari melody LIVE 💚 | {{.Date}}
|
||||||
66
video/video.go
Normal file
66
video/video.go
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
package video
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"arimelody.space/live-vod-uploader/youtube"
|
||||||
|
ffmpeg "github.com/u2takey/ffmpeg-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
probeFormat struct {
|
||||||
|
Duration string `json:"duration"`
|
||||||
|
}
|
||||||
|
probeData struct {
|
||||||
|
Format probeFormat `json:"format"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func ConcatVideo(video *youtube.Video, vodFiles []string) error {
|
||||||
|
fileListPath := path.Join(
|
||||||
|
path.Dir(video.Filename),
|
||||||
|
"files.txt",
|
||||||
|
)
|
||||||
|
|
||||||
|
totalDuration := float64(0.0)
|
||||||
|
fileListString := ""
|
||||||
|
for _, file := range vodFiles {
|
||||||
|
fileListString += fmt.Sprintf("file '%s'\n", file)
|
||||||
|
jsonProbe, err := ffmpeg.Probe(path.Join(path.Dir(video.Filename), file))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to probe file `%s`: %v", file, err)
|
||||||
|
}
|
||||||
|
probe := probeData{}
|
||||||
|
json.Unmarshal([]byte(jsonProbe), &probe)
|
||||||
|
duration, err := strconv.ParseFloat(probe.Format.Duration, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse duration of file `%s`: %v", file, err)
|
||||||
|
}
|
||||||
|
totalDuration += duration
|
||||||
|
}
|
||||||
|
err := os.WriteFile(
|
||||||
|
fileListPath,
|
||||||
|
[]byte(fileListString),
|
||||||
|
0o644,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write file list: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ffmpeg.Input(fileListPath, ffmpeg.KwArgs{
|
||||||
|
"f": "concat",
|
||||||
|
"safe": "0",
|
||||||
|
}).Output(video.Filename, ffmpeg.KwArgs{
|
||||||
|
"c": "copy",
|
||||||
|
}).OverWriteOutput().ErrorToStdOut().Run()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ffmpeg error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
231
youtube/youtube.go
Normal file
231
youtube/youtube.go
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
package youtube
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"arimelody.space/live-vod-uploader/scanner"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"google.golang.org/api/option"
|
||||||
|
"google.golang.org/api/youtube/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DEFAULT_TAGS = []string{
|
||||||
|
"ari melody",
|
||||||
|
"ari melody LIVE",
|
||||||
|
"livestream",
|
||||||
|
"vtuber",
|
||||||
|
"twitch",
|
||||||
|
"gaming",
|
||||||
|
"let's play",
|
||||||
|
"full VOD",
|
||||||
|
"VOD",
|
||||||
|
"stream",
|
||||||
|
"archive",
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
CATEGORY_GAMING = "20"
|
||||||
|
CATEGORY_ENTERTAINMENT = "24"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TitleType int
|
||||||
|
const (
|
||||||
|
TITLE_GAME TitleType = iota
|
||||||
|
TITLE_OTHER
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Title struct {
|
||||||
|
Name string
|
||||||
|
Type TitleType
|
||||||
|
Url string
|
||||||
|
}
|
||||||
|
|
||||||
|
Video struct {
|
||||||
|
Title *Title
|
||||||
|
Part int
|
||||||
|
Date time.Time
|
||||||
|
Tags []string
|
||||||
|
Filename string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildVideo(metadata *scanner.Metadata) (*Video, error) {
|
||||||
|
var titleType TitleType
|
||||||
|
switch metadata.Category.Type {
|
||||||
|
case "gaming":
|
||||||
|
titleType = TITLE_GAME
|
||||||
|
default:
|
||||||
|
titleType = TITLE_OTHER
|
||||||
|
}
|
||||||
|
|
||||||
|
videoDate, err := time.Parse("2006-01-02", metadata.Date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse date from metadata: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Video{
|
||||||
|
Title: &Title{
|
||||||
|
Name: metadata.Category.Name,
|
||||||
|
Type: titleType,
|
||||||
|
Url: metadata.Category.Url,
|
||||||
|
},
|
||||||
|
Part: metadata.Part,
|
||||||
|
Date: videoDate,
|
||||||
|
Tags: DEFAULT_TAGS,
|
||||||
|
Filename: path.Join(
|
||||||
|
metadata.FootageDir,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"%s-fullvod.mkv",
|
||||||
|
videoDate.Format("2006-01-02"),
|
||||||
|
)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
MetaTitle struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Url string
|
||||||
|
}
|
||||||
|
|
||||||
|
Metadata struct {
|
||||||
|
Date string
|
||||||
|
Title *MetaTitle
|
||||||
|
Part int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var titleTemplate *template.Template = template.Must(
|
||||||
|
template.ParseFiles("template/title.txt"),
|
||||||
|
)
|
||||||
|
func BuildTitle(video *Video) (string, error) {
|
||||||
|
var titleType string
|
||||||
|
switch video.Title.Type {
|
||||||
|
case TITLE_GAME:
|
||||||
|
titleType = "game"
|
||||||
|
case TITLE_OTHER:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
titleType = "other"
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
titleTemplate.Execute(out, Metadata{
|
||||||
|
Date: strings.ToLower(video.Date.Format("02 Jan 2006")),
|
||||||
|
Title: &MetaTitle{
|
||||||
|
Name: video.Title.Name,
|
||||||
|
Type: titleType,
|
||||||
|
Url: video.Title.Url,
|
||||||
|
},
|
||||||
|
Part: video.Part,
|
||||||
|
})
|
||||||
|
|
||||||
|
return strings.TrimSpace(out.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var descriptionTemplate *template.Template = template.Must(
|
||||||
|
template.ParseFiles("template/description.txt"),
|
||||||
|
)
|
||||||
|
func BuildDescription(video *Video) (string, error) {
|
||||||
|
var titleType string
|
||||||
|
switch video.Title.Type {
|
||||||
|
case TITLE_GAME:
|
||||||
|
titleType = "game"
|
||||||
|
case TITLE_OTHER:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
titleType = "other"
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &bytes.Buffer{}
|
||||||
|
descriptionTemplate.Execute(out, Metadata{
|
||||||
|
Date: strings.ToLower(video.Date.Format("02 Jan 2006")),
|
||||||
|
Title: &MetaTitle{
|
||||||
|
Name: video.Title.Name,
|
||||||
|
Type: titleType,
|
||||||
|
Url: video.Title.Url,
|
||||||
|
},
|
||||||
|
Part: video.Part,
|
||||||
|
})
|
||||||
|
|
||||||
|
return out.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadVideo(ctx context.Context, token *oauth2.Token, video *Video) error {
|
||||||
|
title, err := BuildTitle(video)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build title: %v", err)
|
||||||
|
}
|
||||||
|
description, err := BuildDescription(video)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build description: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err := youtube.NewService(
|
||||||
|
ctx,
|
||||||
|
option.WithScopes(youtube.YoutubeUploadScope),
|
||||||
|
option.WithTokenSource(oauth2.StaticTokenSource(token)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create youtube service: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
videoService := youtube.NewVideosService(service)
|
||||||
|
|
||||||
|
var categoryId string
|
||||||
|
switch video.Title.Type {
|
||||||
|
case TITLE_GAME:
|
||||||
|
categoryId = CATEGORY_GAMING
|
||||||
|
default:
|
||||||
|
categoryId = CATEGORY_ENTERTAINMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
call := videoService.Insert([]string{
|
||||||
|
"snippet", "status",
|
||||||
|
}, &youtube.Video{
|
||||||
|
Snippet: &youtube.VideoSnippet{
|
||||||
|
Title: title,
|
||||||
|
Description: description,
|
||||||
|
Tags: append(DEFAULT_TAGS, video.Tags...),
|
||||||
|
CategoryId: categoryId, // gaming
|
||||||
|
},
|
||||||
|
Status: &youtube.VideoStatus{
|
||||||
|
PrivacyStatus: "private",
|
||||||
|
},
|
||||||
|
}).NotifySubscribers(false)
|
||||||
|
|
||||||
|
file, err := os.Open(video.Filename)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to open file: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
call.Media(file)
|
||||||
|
|
||||||
|
log.Println("Uploading video...")
|
||||||
|
|
||||||
|
res, err := call.Do()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to upload video: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(res, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to marshal video data json: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(string(data))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue