This commit is contained in:
BlackTeay 2024-05-07 13:53:47 +08:00
parent 7938f80b8e
commit cf0f0f3e8a
5 changed files with 431 additions and 110 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -1,6 +1,6 @@
accesskey: "TOKEN_f7c84db0-1671-4a70-953e-558cb25a4d74"
secretkey: "7de3463e-0ad8-4f28-ba26-b5012f78f2dc"
endpoint: "ufile.cn-bj.ucloud.cn"
accesskey: "TOKEN_6096c736-12b7-4c20-bfa5-e85e0e9c9b65"
secretkey: "cc8a5965-3325-4231-9f64-a6626f624049"
endpoint: "cn-bj.ufileos.com"
encrypt: "false"
enablessl: "true"
language: "ZH"

1
go.mod
View File

@ -3,6 +3,7 @@ module hls-builder
go 1.22.0
require (
fyne.io/fyne v1.4.3
fyne.io/fyne/v2 v2.4.5
github.com/google/uuid v1.1.2
)

30
go.sum
View File

@ -37,12 +37,16 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
fyne.io/fyne v1.4.3 h1:356CnXCiYrrfaLGsB7qLK3c6ktzyh8WR05v/2RBu51I=
fyne.io/fyne v1.4.3/go.mod h1:8kiPBNSDmuplxs9WnKCkaWYqbcXFy0DeAzwa6PBO9Z8=
fyne.io/fyne/v2 v2.4.5 h1:W6jpAEmLoBbKyBB+EXqI7GMJ7kLgHQWCa0wZHUV2VfQ=
fyne.io/fyne/v2 v2.4.5/go.mod h1:SlOgbca0y80cRObu/JOhxIJdIgtoW7aCyqUVlTMgs0Y=
fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e h1:Hvs+kW2VwCzNToF3FmnIAzmivNgrclwPgoUdVSrjkP8=
fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@ -82,12 +86,15 @@ github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 h1:+31CdF/okdokeFN
github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504/go.mod h1:gLRWYfYnMA9TONeppRSikMdXlHQ97xVsPojddUv3b/E=
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 h1:hnLq+55b7Zh7/2IRzWCpiTcAvjv/P8ERF+N7+xXbZhk=
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2/go.mod h1:eO7W361vmlPOrykIg+Rsh1SZ3tQBaOsfzZhsIOb/Lm0=
github.com/fyne-io/mobile v0.1.2/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk=
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240306074159-ea2d69986ecb h1:S9I8pIVT5JHKDvmI1vQ0qs5fqxzUfhcZm/YbUC/8k1k=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240306074159-ea2d69986ecb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -97,10 +104,13 @@ github.com/go-text/typesetting v0.1.0 h1:vioSaLPYcHwPEPLT7gsjCGDCoYSbljxoHJzMnKw
github.com/go-text/typesetting v0.1.0/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
github.com/go-text/typesetting-utils v0.0.0-20240329101916-eee87fb235a3 h1:levTnuLLUmpavLGbJYLJA7fQnKeS7P1eCdAlM+vReXk=
github.com/go-text/typesetting-utils v0.0.0-20240329101916-eee87fb235a3/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8=
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
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=
@ -194,6 +204,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
@ -203,11 +215,11 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@ -225,9 +237,14 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -244,14 +261,18 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -313,6 +334,7 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -436,6 +458,7 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -492,6 +515,7 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@ -513,6 +537,7 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@ -641,8 +666,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

504
main.go
View File

@ -1,7 +1,7 @@
/*
* @Author: BlackTeay
* @Date: 2024-04-30 09:37:39
* @LastEditTime: 2024-05-06 17:33:58
* @LastEditTime: 2024-05-07 13:53:30
* @LastEditors: BlackTeay
* @Description:
* @FilePath: /hls_builder/main.go
@ -42,24 +42,56 @@ var ttfBytes []byte
//go:embed bin/*
var ffmpegFS embed.FS
//go:embed config/userconfig.yaml
var configFS embed.FS
// MyTheme 定义了一个自定义的主题结构体。
type MyTheme struct{}
// Color 根据给定的主题颜色名称和变种返回相应的颜色。
// 参数 c 为主题颜色的名称。
// 参数 v 为颜色的变种。
// 返回值为符合 color.Color 接口的颜色值。
func (MyTheme) Color(c fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
return theme.DefaultTheme().Color(c, v)
}
// Font 根据给定的文本样式返回相应的字体资源。
// 参数 s 为文本的样式,决定了使用的字体。
// 返回值为一个 fyne.Resource 类型的字体资源。
func (MyTheme) Font(s fyne.TextStyle) fyne.Resource {
return fyne.NewStaticResource("./font/NotoSansSC-Regular.ttf", ttfBytes)
}
// Icon 根据给定的图标名称返回相应的图标资源。
// 参数 n 为图标的名称。
// 返回值为一个 fyne.Resource 类型的图标资源。
func (MyTheme) Icon(n fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(n)
}
// Size 根据给定的主题尺寸名称返回相应的尺寸值。
// 参数 s 为主题尺寸的名称。
// 返回值为相应的尺寸值,单位为 float32。
func (MyTheme) Size(s fyne.ThemeSizeName) float32 {
return theme.DefaultTheme().Size(s)
}
func getFFmpegPath() string {
type UploadStats struct {
Successed int
Failed int
TotalSize string
AverageSpeed string
Elapsed string
}
// getFFmpegPath 获取FFmpeg可执行文件的路径。
// 该函数根据运行时操作系统选择对应的FFmpeg文件将该文件从嵌入的文件系统读出
// 并写入到一个临时目录中,然后返回这个临时文件的路径。
// 返回值:
//
// string - FFmpeg可执行文件的临时路径。
func getFFmpegPath() (string, func(), error) {
var ffmpegFileName string
switch runtime.GOOS {
case "windows":
@ -68,13 +100,13 @@ func getFFmpegPath() string {
ffmpegFileName = "ffmpeg_darwin"
}
// 读取文件内容
// 根据操作系统选择的FFmpeg文件名从嵌入的文件系统中读取文件内容
data, err := ffmpegFS.ReadFile("bin/" + ffmpegFileName)
if err != nil {
log.Fatal(err)
}
// 写入到临时文件中
// 创建临时目录并将FFmpeg文件内容写入该目录中的文件
tmpDir, err := os.MkdirTemp("", "ffmpeg")
if err != nil {
log.Fatal(err)
@ -83,10 +115,19 @@ func getFFmpegPath() string {
if err := os.WriteFile(tmpFilePath, data, 0755); err != nil {
log.Fatal(err)
}
cleanup := func() {
os.RemoveAll(tmpDir) // 删除整个临时目录
}
return tmpFilePath
return tmpFilePath, cleanup, nil
}
func getUs3cliPath() string {
// getUs3cliPath 获取us3cli二进制文件的临时路径。
// 该函数根据运行的操作系统选择对应的us3cli文件并将其从嵌入的文件系统中读出
// 然后写入到一个临时文件中,最后返回这个临时文件的路径。
// 返回值:
// - string: us3cli临时文件的路径。
func getUs3cliPath() (string, func(), error) {
var us3cliFileName string
switch runtime.GOOS {
case "windows":
@ -95,13 +136,13 @@ func getUs3cliPath() string {
us3cliFileName = "us3cli-mac"
}
// 读取文件内容
// 根据操作系统选择的us3cli文件名从嵌入的文件系统中读取文件内容
data, err := ffmpegFS.ReadFile("bin/" + us3cliFileName)
if err != nil {
log.Fatal(err)
}
// 写入到临时文件
// 创建临时目录并将us3cli文件内容写入其
tmpDir, err := os.MkdirTemp("", "us3cli")
if err != nil {
log.Fatal(err)
@ -110,22 +151,57 @@ func getUs3cliPath() string {
if err := os.WriteFile(tmpFilePath, data, 0755); err != nil {
log.Fatal(err)
}
cleanup := func() {
os.RemoveAll(tmpDir) // 删除整个临时目录
}
return tmpFilePath
return tmpFilePath, cleanup, nil
}
func getUs3Config() (string, func(), error) {
// 读取嵌入的配置文件
data, err := configFS.ReadFile("config/userconfig.yaml")
if err != nil {
log.Fatal(err)
}
// 创建临时目录并将us3cli文件内容写入其中
tmpDir, err := os.MkdirTemp("", "config")
if err != nil {
log.Fatal(err)
}
tmpFilePath := filepath.Join(tmpDir, "userconfig.yaml")
if err := os.WriteFile(tmpFilePath, data, 0755); err != nil {
log.Fatal(err)
}
cleanup := func() {
os.RemoveAll(tmpDir) // 删除整个临时目录
}
return tmpFilePath, cleanup, nil
}
// main 是应用程序的入口点。
func main() {
//打印程序当前路径
// 打印当前工作目录
log.Println("Current path:", os.Getenv("PWD"))
// 初始化应用并设置主题
myApp := app.NewWithID("hls_builder")
myApp.Settings().SetTheme(MyTheme{})
myWindow := myApp.NewWindow("HLS-builder 视频切片工具")
// 设置窗口大小
myWindow.Resize(fyne.NewSize(800, 600)) // 设置窗口宽为800像素高为600像素
// 创建主窗口并设置大小
myWindow := myApp.NewWindow("HLS-builder 视频切片上传工具")
myWindow.Resize(fyne.NewSize(800, 600))
// 初始化UI组件文件选择提示、输出路径选择提示、进度条
inputFile := widget.NewLabel("请选择mp4文件!")
outputDir := widget.NewLabel("请选择输出路径!")
progressBar := widget.NewProgressBar()
sliceProgressBar := widget.NewProgressBar()
uploadProgressBar := widget.NewProgressBar()
// 创建选择视频文件按钮并设置点击事件
btnSelectFile := widget.NewButton("选择mp4文件", func() {
// 设置文件类型过滤器
filter := storage.NewExtensionFileFilter([]string{".mp4"})
fileDialog := dialog.NewFileOpen(func(file fyne.URIReadCloser, err error) {
if err != nil {
@ -137,10 +213,11 @@ func main() {
inputFile.SetText(file.URI().Path())
}
}, myWindow)
fileDialog.SetFilter(filter) // 设置文件过滤器
fileDialog.SetFilter(filter)
fileDialog.Show()
})
// 创建选择输出路径按钮并设置点击事件
btnSelectOutput := widget.NewButton("选择输出路径", func() {
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
if err == nil && uri != nil {
@ -149,235 +226,324 @@ func main() {
}
}, myWindow)
})
var btnSlice *widget.Button // 预先声明btnSlice变量
var randDir string
var hlsDir string
// 初始化上传按钮
uploadLabel := widget.NewLabel("上传切片到Ucloud:")
var btnUpload *widget.Button
btnUpload = widget.NewButton("上传", func() {
btnUpload.Text = "正在上传..."
btnUpload.Disable()
// 调用上传函数,并在上传完成后启用按钮
go upload(hlsDir, randDir, uploadProgressBar, myWindow, func() {
btnUpload.Text = "上传"
btnUpload.Enable()
})
})
// 初始化切片按钮及相关变量
var btnSlice *widget.Button
btnSlice = widget.NewButton("开始切片", func() {
if inputFile.Text == "没有选择视频文件" || outputDir.Text == "没有选择输出路径" {
fmt.Println(inputFile.Text)
fmt.Println(outputDir.Text)
// 验证用户选择
if inputFile.Text == "请选择mp4文件!" || outputDir.Text == "请选择输出路径!" {
dialog.ShowError(errors.New("请选择视频文件和输出路径"), myWindow)
return
}
// 禁用按钮
// 禁用UI元素以防止用户干预
btnSelectFile.Disable()
btnSelectOutput.Disable()
btnSlice.Disable()
btnSlice.SetText("切片中...")
// 生成随机目录名并在输出路径下创建该目录
randDir = uuid.New().String()
//在outputDir中创建一个文件夹名为一个UUID字符串
hlsDir = outputDir.Text + "/" + randDir
//新建这个目录
err := os.Mkdir(hlsDir, 0755)
if err != nil {
dialog.ShowError(errors.New("建立随机目录出错!"), myWindow)
}
go sliceVideo(inputFile.Text, hlsDir, progressBar, func() {
// UI updates directly in the callback
// 调用切片函数并在切片完成后更新UI
go sliceVideo(inputFile.Text, hlsDir, sliceProgressBar, func() {
btnSelectFile.Enable()
btnSelectOutput.Enable()
btnSlice.Enable()
btnSlice.SetText("开始切片")
// You may also want to display a dialog or notification
// dialog.ShowInformation("完成", "Video slicing completed successfully.", myWindow)
}, btnSelectFile, btnSelectOutput, btnSlice, myWindow)
}, btnSelectFile, btnSelectOutput, btnSlice, btnUpload, myWindow)
})
})
uploadLabel := widget.NewLabel("上传切片到Ucloud")
var btnUpload *widget.Button
btnUpload = widget.NewButton("上传到服务器", func() {
go upload(hlsDir, randDir, func() {
btnUpload.Enable()
})
})
// btnUpload.Disable()
btnUpload.Disable()
// 组合UI元素
content := container.NewVBox(
inputFile,
btnSelectFile,
outputDir,
btnSelectOutput,
progressBar,
sliceProgressBar,
btnSlice,
uploadLabel,
btnUpload,
uploadProgressBar,
)
// 设置窗口内容并显示
myWindow.SetContent(content)
myWindow.ShowAndRun()
}
func sliceVideo(inputFile, outputDir string, progressBar *widget.ProgressBar, onComplete func(), btnSelectFile, btnSelectOutput, btnSlice *widget.Button, myWindow fyne.Window) {
ffmpegPath := getFFmpegPath()
startTime := time.Now() // 记录开始时间
// sliceVideo 切割视频文件为HLS格式的多个段。
//
// 参数:
// inputFile - 输入视频文件的路径。
// outputDir - 输出目录的路径,切片和播放列表将保存于此目录。
// progressBar - 用于显示切割过程进度的进度条。
// onComplete - 切割完成后调用的回调函数。
// btnSelectFile, btnSelectOutput, btnSlice - 用于用户界面交互的按钮。
// myWindow - 所属的FYNE窗口用于显示对话框和其他UI元素。
//
// 此函数使用FFmpeg工具切割视频将视频文件分解为多个时长为5秒的HLS段并创建一个播放列表。
// 过程中会更新进度条,并在完成时显示一个通知对话框。
func sliceVideo(inputFile, outputDir string, progressBar *widget.ProgressBar, onComplete func(), btnSelectFile, btnSelectOutput, btnSlice *widget.Button, btnUpload *widget.Button, myWindow fyne.Window) {
// 获取FFmpeg可执行文件的路径
ffmpegPath, cleanup, err := getFFmpegPath()
if err != nil {
log.Fatal(err)
}
defer cleanup() // 确保在函数返回时删除临时目录
startTime := time.Now() // 记录开始时间以便计算总耗时
log.Println("开始切割视频")
// 调整 FFmpeg 命令以输出调试信息,有助于解析
// 构建FFmpeg命令行配置输出为HLS格式每5秒切一片
cmd := exec.Command(
ffmpegPath,
"-i", inputFile, // 输入文件
"-c", "copy", // 复制编解码器设置
"-start_number", "0", // HLS 段的起始编号
"-hls_time", "5", // 每个 HLS 段的最大时长(秒)
"-c", "copy", // 复制编解码器设置,避免重新编码
"-start_number", "0", // HLS段的起始编号
"-hls_time", "5", // 每个HLS段的最大时长
"-hls_list_size", "0", // 播放列表中的最大段数0表示无限制
"-f", "hls", // 输出格式为 HLS
"-f", "hls", // 输出格式为HLS
"-hls_segment_filename", outputDir+"/%08d.ts", // 段文件的命名方式
outputDir+"/playlist.m3u8", // 输出的 m3u8 文件位置
outputDir+"/playlist.m3u8", // 输出的m3u8文件位置
)
// cmd.Stderr = os.Stderr // 将 stderr 直接定向到系统的 stderr以便直接调试 FFmpeg 的输出
cmdReader, err := cmd.StderrPipe() // 现在使用 stderr 管道而不是 stdout
// 设置管道以读取FFmpeg的stderr用于解析进度信息
cmdReader, err := cmd.StderrPipe()
if err != nil {
log.Fatal("创建 stderr 管道失败:", err)
log.Fatal("创建stderr管道失败:", err)
}
if err := cmd.Start(); err != nil {
log.Fatalf("cmd.Start() 失败: %s\n", err)
log.Fatalf("cmd.Start()失败: %s\n", err)
}
scanner := bufio.NewScanner(cmdReader)
totalDuration := getTotalDuration(inputFile) // 确保此函数正确获取总持续时间
totalDuration := getTotalDuration(inputFile) // 获取视频的总时长
log.Printf("视频总时长%f\n", totalDuration)
segment := math.Ceil(totalDuration / 5)
segment := math.Ceil(totalDuration / 5) // 计算所需的段数
log.Println(segment)
// currentDuration := 0.0
// 在后台goroutine中监控FFmpeg的输出更新进度条并处理完成状态
go func() {
var completed bool
for scanner.Scan() {
line := scanner.Text()
log.Println("FFmpeg 输出:", line)
log.Println("FFmpeg输出:", line)
// 解析FFmpeg的输出更新进度条
if segmentCount := parseFFmpegProgressOutput(line); segmentCount > 0 {
log.Println(segmentCount / segment)
progressBar.SetValue(segmentCount / segment)
}
// 检查是否输出了完成的标志
// 检查是否已完成切割
if checkFFmpegCompletion(line) {
completed = true
endTime := time.Now() // 记录完成时间
duration := endTime.Sub(startTime) // 计算耗时
formattedDuration := formatDuration(duration) // 格式化显示耗时
progressBar.SetValue(1.0) // 设置进度条到最大值
fyne.CurrentApp().SendNotification(&fyne.Notification{
Title: "HLS-builder 视频切片工具",
Content: fmt.Sprintf("✅ 视频切片完成! 总用时: %s\n存储位置%s", formattedDuration, outputDir),
})
dialog.ShowInformation("完成", fmt.Sprintf("✅ 视频切片完成! 总用时: %s\n存储位置%s", formattedDuration, outputDir), myWindow)
duration := endTime.Sub(startTime) // 计算总耗时
formattedDuration := formatDuration(duration) // 格式化耗时显示
progressBar.SetValue(1.0) // 设置进度条为完成状态
// 显示完成信息的对话框
dialog.ShowInformation("完成", fmt.Sprintf("✅ 视频切片完成!可以将切片直接上传云存储。\n总用时: %s\n存储位置%s", formattedDuration, outputDir), myWindow)
log.Println("视频切片完成")
btnUpload.Enable()
break
}
}
if err := cmd.Wait(); err != nil {
log.Fatalf("cmd.Wait() 失败: %s\n", err)
log.Fatalf("cmd.Wait()失败: %s\n", err)
}
if !completed {
log.Println("❌ 视频切片可能未正确完成")
}
onComplete()
onComplete() // 调用完成回调
}()
}
// getTotalDuration extracts total duration of the video file using FFmpeg
// getTotalDuration 使用 FFmpeg 提取视频文件的总时长
// 参数:
//
// file string - 视频文件的路径
//
// 返回值:
//
// float64 - 视频的总时长(单位:秒)
func getTotalDuration(file string) float64 {
ffmpegPath := getFFmpegPath()
// 构建和启动 ffmpeg 命令
// 获取 FFmpeg 可执行文件的路径
ffmpegPath, cleanup, err := getFFmpegPath()
if err != nil {
log.Fatal(err)
}
defer cleanup() // 确保在函数返回时删除临时目录
// 构建并启动 FFmpeg 命令,用于获取视频时长
cmd := exec.Command(ffmpegPath, "-i", file)
// 由于 ffmpeg 在只有 '-i' 时输出到 stderr因此需要捕获 stderr
// 由于 FFmpeg 在只有 "-i" 参数时将输出发送到 stderr因此我们需要捕获 stderr
stderr, err := cmd.StderrPipe()
if err != nil {
log.Fatal("Failed to get stderr pipe:", err)
}
// 启动 FFmpeg 命令
if err := cmd.Start(); err != nil {
log.Println("Failed to start ffmpeg:", err)
return 0.0 // 这里选择不是致命错误只是返回0.0表示无法获取时长
return 0.0 // 无法获取时长时返回 0.0
}
// 使用 Scanner 来读取 stderr
// 使用 Scanner 从 stderr 中读取输出
scanner := bufio.NewScanner(stderr)
// 正则表达式用于匹配 FFmpeg 输出的时长信息
durationRegex := regexp.MustCompile(`Duration: (\d{2}):(\d{2}):(\d{2}\.\d{2})`)
var duration float64 = 0.0
for scanner.Scan() {
// 寻找匹配的时长字符串,并解析为秒
matches := durationRegex.FindStringSubmatch(scanner.Text())
if matches != nil {
hours, _ := strconv.Atoi(matches[1])
minutes, _ := strconv.Atoi(matches[2])
seconds, _ := strconv.ParseFloat(matches[3], 64)
duration = float64(hours)*3600 + float64(minutes)*60 + seconds
break
break // 找到时长后即刻跳出循环
}
}
// 等待命令完成,忽略错误
cmd.Wait() // 注意这里我们不处理错误,因为 FFmpeg 总是返回错误状态
// 等待 FFmpeg 命令执行完成,不处理命令结束状态
cmd.Wait()
return duration
}
// parseFFmpegProgressOutput 函数解析 FFmpeg 进度输出行,返回匹配到的数字部分。
// 参数:
//
// line string - FFmpeg 进度输出的一行文本。
//
// 返回值:
//
// float64 - 匹配到的数字转换为浮点数后返回,如果没有匹配到则返回 0。
func parseFFmpegProgressOutput(line string) float64 {
re := regexp.MustCompile(`(\d+)\.ts`) // 正则表达式匹配数字后跟.ts
re := regexp.MustCompile(`(\d+)\.ts`) // 使用正则表达式编译模式以匹配数字后跟.ts的字符串
matches := re.FindStringSubmatch(line)
if len(matches) > 1 {
number, err := strconv.Atoi(matches[1]) // 将提取的数字转换为int
number, err := strconv.Atoi(matches[1]) // 将提取的数字字符串转换为int类型
if err != nil {
log.Println("Error converting string to integer:", err)
log.Println("Error converting string to integer:", err) // 记录转换错误日志
return 0
}
return float64(number)
return float64(number) // 返回匹配到的数字转换后的浮点数
}
return 0 // 如果没有找到匹配返回0
return 0 // 如果没有找到匹配的模式返回0
}
// checkFFmpegCompletion 函数用于检查 FFmpeg 处理进程是否完成。
// 它通过分析 FFmpeg 输出的行中是否包含特定的字符串来判断处理是否完成。
//
// 参数:
// line string - FFmpeg 输出的一行文本。
//
// 返回值:
// bool - 如果行中包含指定的字符串,则返回 true表示处理已完成否则返回 false。
func checkFFmpegCompletion(line string) bool {
// 记录接收到的行,以便于调试
log.Println("line:", line)
// 正则表达式匹配含有 "out#0/hls" 的行,这通常在 FFmpeg 输出最后阶段显示
re := regexp.MustCompile(`out#0/hls`)
// 检查这一行是否匹配正则表达式
matchFound := re.MatchString(line)
log.Println("re", matchFound)
return matchFound
}
// formatDuration 将时间Duration格式化为易读的字符串
// 参数:
//
// d time.Duration - 需要格式化的时间长度
//
// 返回值:
//
// string - 格式化后的时间字符串,格式为 "X小时X分X秒X毫秒",根据实际时间长度包含其中的项
func formatDuration(d time.Duration) string {
// 将时间转换为总秒数
totalSeconds := int(d.Seconds())
// 计算小时、分钟和秒
hours := totalSeconds / 3600
minutes := (totalSeconds % 3600) / 60
seconds := totalSeconds % 60
milliseconds := int(d.Milliseconds()) % 1000 // 获取剩余的毫秒数
// 获取毫秒数
milliseconds := int(d.Milliseconds()) % 1000
result := ""
// 如果有小时,则添加到结果中
if hours > 0 {
result += fmt.Sprintf("%d小时", hours)
}
if minutes > 0 || hours > 0 { // 显示分钟,如果有小时无论如何都显示
// 如果有小时或者分钟不为0则添加分钟到结果中
if minutes > 0 || hours > 0 {
result += fmt.Sprintf("%d分", minutes)
}
// 只在秒大于0或小时和分钟都为0时显示秒
// 如果秒不为0或者同时小时和分钟都不为0则添加秒到结果中
if seconds > 0 || (hours != 0 && minutes != 0) {
result += fmt.Sprintf("%d秒", seconds)
}
// 总是添加毫秒到结果中
result += fmt.Sprintf("%d毫秒", milliseconds)
return result
}
// 上传到Ucloud
func upload(localPath string, keyPath string, onComplete func()) {
func upload(localPath string, keyPath string, progressBar *widget.ProgressBar, myWindow fyne.Window, onComplete func()) {
fmt.Println("上传到Ucloud")
fmt.Println("localPath:", localPath)
fmt.Println("keyPath:", keyPath)
// 统计 .ts 文件数量
tsCount, err := countTSFiles(localPath)
// 统计 .ts .m3u8 文件数量
tsFiles, m3u8Files, err := countMediaFiles(localPath)
if err != nil {
log.Fatalf("Failed to count .ts files: %s", err)
} else {
fmt.Printf("Found %d .ts files in %s\n", tsCount, localPath)
log.Fatalf("Error counting media files: %s", err)
}
us3cliPath := getUs3cliPath()
fmt.Printf("Total .ts files: %d\n", tsFiles)
fmt.Printf("Total .m3u8 files: %d\n", m3u8Files)
filesTotal := float64(tsFiles + m3u8Files)
fmt.Printf("Total files: %d\n", int(filesTotal))
us3cliPath, cleanup, err := getUs3cliPath()
if err != nil {
log.Fatal(err)
}
defer cleanup() // 确保在函数返回时删除临时目录
bucket := "us3://jlntv-live/replay/" + keyPath
cmd := exec.Command(us3cliPath, "cp", localPath, bucket, "-r", "--parallel", "20")
us3ConfigPath, cleanup, err := getUs3Config()
if err != nil {
log.Fatal(err)
}
defer cleanup() // 确保在函数返回时删除临时目录
cmd := exec.Command(us3cliPath, "cp", localPath, bucket, "-r", "--parallel", "20", "--config", us3ConfigPath)
//输出cmd
fmt.Println(cmd.String())
stdout, err := cmd.StdoutPipe()
if err != nil {
@ -387,13 +553,64 @@ func upload(localPath string, keyPath string, onComplete func()) {
if err := cmd.Start(); err != nil {
log.Fatalf("Failed to start command: %s", err)
}
progress := 0.0
go func() {
buffer := make([]byte, 4096) // 创建一个足够大的buffer
for {
n, err := stdout.Read(buffer)
if n > 0 {
fmt.Print("STDOUT:", string(buffer[:n])) // 打印实时输出
filesUploadedTotal, err := extractTotalValue(string(buffer[:n]))
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Total:", filesUploadedTotal)
}
stats, err := parseUploadStats(string(buffer[:n]))
if err != nil {
fmt.Println("Error parsing upload stats:", err)
} else {
fmt.Printf("Uploaded: %d, Failed: %d, Total Size: %s, Speed: %s\n",
stats.Successed, stats.Failed, stats.TotalSize, stats.AverageSpeed)
if float64(stats.Successed) == filesTotal {
progressBar.SetValue(1)
showCustomDialog := func() {
replayURL := fmt.Sprintf("https://video.jlntv.cn/replay/%s/playlist.m3u8", keyPath)
copyBtn := widget.NewButton("复制地址", func() {
fmt.Println("copyed:", replayURL)
// dialog.ShowInformation("Submitted", "You submitted: "+input.Text, myWindow)
clipboard := myWindow.Clipboard()
clipboard.SetContent(replayURL)
// showToast(myWindow, "地址已复制到剪贴板", 3*time.Second)
dialog.ShowInformation("提示", "地址已复制到剪贴板!", myWindow)
})
// 创建自定义内容的容器
content := container.NewVBox(
widget.NewLabel("Upload Stats:"),
widget.NewLabel(fmt.Sprintf("用时:%s秒", stats.Elapsed)),
widget.NewLabel(fmt.Sprintf("成功:%d", stats.Successed)),
widget.NewLabel(fmt.Sprintf("失败:%d", stats.Failed)),
widget.NewLabel(fmt.Sprintf("平均速度:%s", stats.AverageSpeed)),
widget.NewLabel(fmt.Sprintf("总大小:%s", stats.TotalSize)),
widget.NewLabel(fmt.Sprintf("回看地址: %s", replayURL)),
copyBtn,
)
// 设置内容容器的最小尺寸
content.Resize(fyne.NewSize(500, 600)) // 你可以根据需要调整这个尺寸
// 创建并显示自定义对话框
customDialog := dialog.NewCustom("上传成功", "关闭", content, myWindow)
customDialog.Show()
}
showCustomDialog()
// dialog.ShowInformation("完成", fmt.Sprintf("✅ HLS上传完毕\n总用时:%s秒\n成功:%d 失败:%d 总大小:%s 平均速度:%s", stats.Elapsed, stats.Successed, stats.Failed, stats.TotalSize, stats.AverageSpeed), myWindow)
fmt.Println("Upload completed!")
} else {
progress = filesUploadedTotal / filesTotal
fmt.Println("Progress:", progress)
progressBar.SetValue(progress)
}
}
}
if err != nil {
break
@ -410,24 +627,101 @@ func upload(localPath string, keyPath string, onComplete func()) {
}
}
// countTSFiles 统计指定目录下所有 .ts 文件的数量。
func extractTotalValue(text string) (float64, error) {
re := regexp.MustCompile(`Total:(\d+)`)
matches := re.FindStringSubmatch(text)
if len(matches) > 1 {
total, err := strconv.Atoi(matches[1])
if err != nil {
return 0, err // 在转换过程中发生错误
}
return float64(total), nil
}
return 200, fmt.Errorf("no total found in the text")
}
func parseUploadStats(line string) (*UploadStats, error) {
stats := &UploadStats{}
var err error
successedRegex := regexp.MustCompile(`(\d+) Successed`)
failedRegex := regexp.MustCompile(`(\d+) Failed`)
sizeRegex := regexp.MustCompile(`Size: ([\d.]+ MB)`)
speedRegex := regexp.MustCompile(`Average speed ([\d.]+ MB/s)`)
elapsedRegex := regexp.MustCompile(`Elapsed\s*:\s*([\d.]+)s`)
// Extracting number of files successed
successedMatches := successedRegex.FindStringSubmatch(line)
if len(successedMatches) > 1 {
stats.Successed, err = strconv.Atoi(successedMatches[1])
if err != nil {
return nil, fmt.Errorf("error parsing successed number: %v", err)
}
}
// Extracting number of files failed
failedMatches := failedRegex.FindStringSubmatch(line)
if len(failedMatches) > 1 {
stats.Failed, err = strconv.Atoi(failedMatches[1])
if err != nil {
return nil, fmt.Errorf("error parsing failed number: %v", err)
}
}
// Extracting total size
sizeMatches := sizeRegex.FindStringSubmatch(line)
if len(sizeMatches) > 1 {
stats.TotalSize = sizeMatches[1]
}
// Extracting average speed
speedMatches := speedRegex.FindStringSubmatch(line)
if len(speedMatches) > 1 {
stats.AverageSpeed = speedMatches[1]
}
// Extracting Elapsed
elapsedMatches := elapsedRegex.FindStringSubmatch(line)
if len(elapsedMatches) > 1 {
stats.Elapsed = elapsedMatches[1]
}
return stats, nil
}
// countMediaFiles 统计给定目录中 .ts 和 .m3u8 文件的数量。
//
// 参数:
// directory - 需要统计的目录路径。
//
// directory string - 需要遍历统计文件的目录路径。
//
// 返回值:
// 返回 .ts 文件的数量以及可能出现的错误。
func countTSFiles(directory string) (int, error) {
var count int // 用于记录 .ts 文件数量
//
// int - .ts 文件的数量。
// int - .m3u8 文件的数量。
// error - 遍历目录过程中遇到的任何错误。
func countMediaFiles(directory string) (int, int, error) {
var tsCount, m3u8Count int // 分别用于记录 .ts 和 .m3u8 文件的数量
// 使用 filepath.Walk 遍历指定目录及其子目录中的所有文件
err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
// 检查遍历过程是否遇到错误
if err != nil {
return err // 如果遍历过程中出现错误,则返回该错误
return err // 如果有错误,则终止遍历并返回该错误
}
// 检查当前路径项是否为文件且扩展名为 ".ts"
if !info.IsDir() && filepath.Ext(info.Name()) == ".ts" {
count++ // 如果满足条件,则计数加一
// 检查当前路径项是否为文件,不遍历目录
if !info.IsDir() {
// 根据文件扩展名统计 .ts 和 .m3u8 文件数量
switch filepath.Ext(info.Name()) {
case ".ts":
tsCount++ // .ts 文件计数
case ".m3u8":
m3u8Count++ // .m3u8 文件计数
}
}
return nil
return nil // 继续遍历下一个文件或目录
})
return count, err // 返回计数结果和可能出现的错误
// 返回 .ts 和 .m3u8 文件的计数结果,以及遍历过程中可能遇到的错误
return tsCount, m3u8Count, err
}