添加音频推送

This commit is contained in:
Sake 2025-12-31 11:29:58 +08:00
parent e222d8b3c5
commit 70a62c2093
59 changed files with 2299 additions and 230 deletions

2
.gitignore vendored
View File

@ -67,5 +67,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
uploads/*
dish*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

After

Width:  |  Height:  |  Size: 647 KiB

View File

@ -17,7 +17,7 @@
}
.img-box {
max-width: 260px;
max-width: 660px;
margin: auto;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

After

Width:  |  Height:  |  Size: 2.2 MiB

10
go.mod
View File

@ -6,11 +6,16 @@ require (
git.lnton.com/lnton/pkg v1.5.27
github.com/gin-contrib/static v1.1.5
github.com/gin-gonic/gin v1.11.0
github.com/google/uuid v1.6.0
github.com/google/wire v0.7.0
github.com/gorilla/websocket v1.5.3
github.com/ixugo/goddd v1.4.0
github.com/jinzhu/copier v0.4.0
github.com/lib/pq v1.10.9
github.com/pelletier/go-toml/v2 v2.2.4
github.com/pion/rtp v1.9.0
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300
github.com/youpy/go-wav v0.3.2
gorm.io/gorm v1.31.0
)
@ -24,8 +29,6 @@ require (
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafov/m3u8 v0.12.1 // indirect
github.com/ixugo/netpulse v0.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
@ -34,12 +37,15 @@ require (
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/panjf2000/ants/v2 v2.11.3 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/shirou/gopsutil/v4 v4.25.7 // indirect
github.com/youpy/go-riff v0.1.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect

75
go.sum
View File

@ -1,34 +1,20 @@
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
git.lnton.com/lnton/pkg v1.5.27 h1:pf4QqL00/yrGtVUdaHo7eZ941D6B9q5oX4ffnjAYWI4=
git.lnton.com/lnton/pkg v1.5.27/go.mod h1:+xvqNpqlxuRthZHiuaMg5Spf5yIgRE63KrmOFLmlp3E=
git.lnton.com/lnton/protocol v0.0.18 h1:QyhNt/J924PaEBz/LIUYOtUiz4Hkp1AMeuZvPa/5W1M=
git.lnton.com/lnton/protocol v0.0.18/go.mod h1:gSXM43E9v4KnCI3FN6iaIB+acvPF6tj+E+m9XekakjY=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
@ -41,7 +27,6 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@ -62,24 +47,20 @@ github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
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.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
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/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafov/m3u8 v0.12.1 h1:DuP1uA1kvRRmGNAZ0m+ObLv1dvrfNO0TPx0c/enNk0s=
github.com/grafov/m3u8 v0.12.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ixugo/goddd v1.4.0 h1:2SMO7wfqVYuZu5KwdvN6aLk1cOY9MgxcJAhClH1yhMQ=
github.com/ixugo/goddd v1.4.0/go.mod h1:FzEjEd6uWEWan1XWTh8VXdqGtyjMYGow/URNtBY8X7w=
github.com/ixugo/netpulse v0.1.1 h1:M7pdwJhpSDuwFdjEgCcanR5lLZgd+4akOstgbyRZOgw=
@ -102,11 +83,8 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
@ -117,69 +95,64 @@ github.com/lestrrat-go/strftime v1.1.1 h1:zgf8QCsgj27GlKBy3SU9/8MMgegZ8UCzlCyHYr
github.com/lestrrat-go/strftime v1.1.1/go.mod h1:YDrzHJAODYQ+xxvrn5SG01uFIQAeDTzpxNVppCz7Nmw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
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/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtp v1.9.0 h1:NL2nGZPXhjnTQGRgsDZRv0ZTo0Or5fkjCy9o9PtBHBU=
github.com/pion/rtp v1.9.0/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vAjVY4qbSr8rQ610YzTkWcxzxSI=
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300/go.mod h1:FNa/dfN95vAYCNFrIKRrlRo+MBLbwmR9Asa5f2ljmBI=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/wenlng/go-captcha-assets v1.0.1/go.mod h1:yQqc7rRbxgLCg+tWtVp+7Y317D1wIZDan/yIwt8wSac=
github.com/wenlng/go-captcha/v2 v2.0.2/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k=
github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU=
github.com/youpy/go-wav v0.3.2/go.mod h1:0FCieAXAeSdcxFfwLpRuEo0PFmAoc+8NU34h7TUvk50=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b h1:QqixIpc5WFIqTLxB3Hq8qs0qImAgBdq0p6rq2Qdl634=
github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8=
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/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
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/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
@ -206,12 +179,10 @@ golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA=
golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -219,20 +190,15 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
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/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
@ -240,7 +206,6 @@ google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@ -248,21 +213,18 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
modernc.org/cc/v4 v4.24.1 h1:mLykA8iIlZ/SZbwI2JgYIURXQMSgmOb/+5jaielxPi4=
modernc.org/cc/v4 v4.24.1/go.mod h1:T1lKJZhXIi2VSqGBiB4LIbKs9NsKTbUXj4IDrmGqtTI=
modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I=
modernc.org/ccgo/v4 v4.23.5 h1:6uAwu8u3pnla3l/+UVUrDDO1HIGxHTYmFH6w+X9nsyw=
modernc.org/ccgo/v4 v4.23.5/go.mod h1:FogrWfBdzqLWm1ku6cfr4IzEFouq2fSAPf6aSAHdAJQ=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.0 h1:Tiw3pezQj7PfV8k4Dzyu/vhRHR2e92kOXtTFU8pbCl4=
modernc.org/gc/v2 v2.6.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.61.5 h1:WzsPUvWl2CvsRmk2foyWWHUEUmQ2iW4oFyWOVR0O5ho=
modernc.org/libc v1.61.5/go.mod h1:llBdEGIywhnRgAFuTF+CWaKV8/2bFgACcQZTXhkAuAM=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
@ -279,4 +241,3 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -49,7 +49,7 @@ func Run(bc *conf.Bootstrap) {
server.ReadTimeout(bc.Server.HTTP.Timeout.Duration()),
server.WriteTimeout(bc.Server.HTTP.Timeout.Duration()),
)
lis, err := net.Listen("tcp", ":8089")
lis, err := net.Listen("tcp", ":0")
if err != nil {
fmt.Printf("创建监听器失败: %v\n", err)
return

View File

@ -17,7 +17,7 @@ type AudioEncodeStorer interface {
Add(context.Context, *AudioEncode) error
Edit(context.Context, *AudioEncode, func(*AudioEncode), ...orm.QueryOption) error
Del(context.Context, *AudioEncode, ...orm.QueryOption) error
EditEnable(enable bool, id int) error
EditStatus(status EncodeStatus, id int) error
}
// FindAudioEncodeAll Paginated search
@ -34,13 +34,16 @@ func (c Core) FindAudioEncodeAll() ([]*AudioEncode, int64, error) {
func (c Core) FindAudioEncode(ctx context.Context, in *FindAudioEncodeInput) ([]*AudioEncode, int64, error) {
items := make([]*AudioEncode, 0)
if in.Name != "" {
total, err := c.store.AudioEncode().Find(ctx, &items, in, orm.Where("name like ? ", "%"+in.Name+"%"))
query := orm.NewQuery(8).
Where("name like ? ", "%"+in.Name+"%").OrderBy("created_at DESC")
total, err := c.store.AudioEncode().Find(ctx, &items, in, query.Encode()...)
if err != nil {
return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
}
return items, total, nil
} else {
total, err := c.store.AudioEncode().Find(ctx, &items, in)
query := orm.NewQuery(2).OrderBy("created_at DESC")
total, err := c.store.AudioEncode().Find(ctx, &items, in, query.Encode()...)
if err != nil {
return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
}
@ -104,9 +107,9 @@ func (c Core) DelAudioEncode(ctx context.Context, id int) (*AudioEncode, error)
return &out, nil
}
// AudioEncodeEnable Update
func (c Core) AudioEncodeEnable(enable bool, id int) error {
if err := c.store.AudioEncode().EditEnable(enable, id); err != nil {
// AudioEncodeStatus Update
func (c Core) AudioEncodeStatus(status EncodeStatus, id int) error {
if err := c.store.AudioEncode().EditStatus(status, id); err != nil {
return reason.ErrDB.Withf(`Enable err[%s]`, err.Error())
}
return nil

View File

@ -31,5 +31,5 @@ type AddAudioEncodeInput struct {
Mode string `json:"mode"` // 文件类型 mp3 wav
Size int64 `json:"size"` // 文件大小
Des string `json:"des"` // 描述
Duration int64 `json:"duration"` // 时长
}

View File

@ -0,0 +1,133 @@
// Code generated by gowebx, DO AVOID EDIT.
package audioencode
import (
"context"
"git.lnton.com/lnton/pkg/orm"
"git.lnton.com/lnton/pkg/reason"
"github.com/jinzhu/copier"
"log/slog"
)
// AudioTaskStorer Instantiation interface
type AudioTaskStorer interface {
Find(context.Context, *[]*AudioTask, orm.Pager, ...orm.QueryOption) (int64, error)
FindAll(dp *[]*AudioTask) (int64, error)
Get(context.Context, *AudioTask, ...orm.QueryOption) error
Add(context.Context, *AudioTask) error
Edit(context.Context, *AudioTask, func(*AudioTask), ...orm.QueryOption) error
Del(context.Context, *AudioTask, ...orm.QueryOption) error
EditStatus(status int, id int) error
EditStatusError(id, status int, s string) error
}
// FindAudioTaskAll Paginated search
func (c Core) FindAudioTaskAll() ([]*AudioTask, int64, error) {
items := make([]*AudioTask, 0)
total, err := c.store.AudioTask().FindAll(&items)
if err != nil {
return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
}
return items, total, nil
}
// FindAudioTask Paginated search
func (c Core) FindAudioTask(ctx context.Context, in *FindAudioTaskInput) ([]*AudioTask, int64, error) {
items := make([]*AudioTask, 0)
if in.Name != "" {
query := orm.NewQuery(8).
Where("audio_name like ? OR channel_id like ? OR channel_name like ?", "%"+in.Name+"%", "%"+in.Name+"%", "%"+in.Name+"%").OrderBy("created_at DESC")
total, err := c.store.AudioTask().Find(ctx, &items, in, query.Encode()...)
if err != nil {
return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
}
return items, total, nil
} else {
query := orm.NewQuery(2).OrderBy("created_at DESC")
total, err := c.store.AudioTask().Find(ctx, &items, in, query.Encode()...)
if err != nil {
return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
}
return items, total, nil
}
}
// GetAudioTask Query a single object
func (c Core) GetAudioTask(ctx context.Context, id int) (*AudioTask, error) {
var out AudioTask
if err := c.store.AudioTask().Get(ctx, &out, orm.Where("id=?", id)); err != nil {
if orm.IsErrRecordNotFound(err) {
return nil, reason.ErrNotFound.Withf(`Get err[%s]`, err.Error())
}
return nil, reason.ErrDB.Withf(`Get err[%s]`, err.Error())
}
return &out, nil
}
func (c Core) GetNameAudioTask(ctx context.Context, name string) (*AudioTask, error) {
var out AudioTask
if err := c.store.AudioTask().Get(ctx, &out, orm.Where("name=?", name)); err != nil {
if orm.IsErrRecordNotFound(err) {
return nil, reason.ErrNotFound.Withf(`Get err[%s]`, err.Error())
}
return nil, reason.ErrDB.Withf(`Get err[%s]`, err.Error())
}
return &out, nil
}
// AddAudioTask Insert into database
func (c Core) AddAudioTask(ctx context.Context, in *AddAudioTask) (*AudioTask, error) {
var out AudioTask
if err := copier.Copy(&out, in); err != nil {
slog.Error("Copy", "err", err)
}
if err := c.store.AudioTask().Add(ctx, &out); err != nil {
return nil, reason.ErrDB.Withf(`Add err[%s]`, err.Error())
}
return &out, nil
}
// EditAudioTask Update object information
func (c Core) EditAudioTask(ctx context.Context, in *EditAudioTaskInput, id int) (*AudioTask, error) {
var out AudioTask
if err := c.store.AudioTask().Edit(ctx, &out, func(b *AudioTask) {
if err := copier.Copy(b, in); err != nil {
slog.Error("Copy", "err", err)
}
}, orm.Where("id=?", id)); err != nil {
return nil, reason.ErrDB.Withf(`Edit err[%s]`, err.Error())
}
return &out, nil
}
// DelAudioTask Delete object
func (c Core) DelAudioTask(ctx context.Context, id int) (*AudioTask, error) {
var out AudioTask
if err := c.store.AudioTask().Del(ctx, &out, orm.Where("id=?", id)); err != nil {
return nil, reason.ErrDB.Withf(`Del err[%s]`, err.Error())
}
return &out, nil
}
func (c Core) DelAudioTaskAll(ctx context.Context, ids []int) (*AudioTask, error) {
var out AudioTask
if err := c.store.AudioTask().Del(ctx, &out, orm.Where("id in (?)", ids)); err != nil {
return nil, reason.ErrDB.Withf(`Del ids err[%s]`, err.Error())
}
return &out, nil
}
// AudioTaskStatus Update
func (c Core) AudioTaskStatus(status int, id int) error {
if err := c.store.AudioTask().EditStatus(status, id); err != nil {
return reason.ErrDB.Withf(`Status err[%s]`, err.Error())
}
return nil
}
// AudioTaskStatus Update
func (c Core) AudioTaskStatusError(id, status int, s string) error {
if err := c.store.AudioTask().EditStatusError(id, status, s); err != nil {
return reason.ErrDB.Withf(`StatusError err[%s]`, err.Error())
}
return nil
}

View File

@ -0,0 +1,47 @@
// Code generated by gowebx, DO AVOID EDIT.
package audioencode
import (
"git.lnton.com/lnton/pkg/web"
)
type FindAudioTaskInput struct {
web.PagerFilter
Name string `form:"name"` // 名称
AudioID int `form:"audio_id"` // 关联音频
AudioName string `form:"audio_name"` // 音频名称
ChannelID string `form:"channel_id"` // 关联通道
ChannelName string `form:"channel_name"` // 拉流地址
}
type EditAudioTaskInput struct {
AudioID int `json:"audio_id"` // 关联音频
ChannelID string `json:"channel_id"` // 关联通道
ChannelName string `json:"channel_name"` // 关联通道名称
}
type AddAudioTaskItem struct {
AudioID int `json:"audio_id"` // 关联音频
ChannelID string `json:"channel_id"` // 关联通道
ChannelName string `json:"channel_name"` // 关联通道名称
}
type AddErrorOutput struct {
AudioID int `json:"audio_id"` // 关联音频
ChannelID string `json:"channel_id"` // 关联通道
ChannelName string `json:"channel_name"` // 关联通道名称
ErrorMsg string `json:"error_msg"` // 错误信息
}
type AddAudioTaskInput struct {
Items []AddAudioTaskItem `json:"items"`
}
type AddAudioTask struct {
AudioID int `json:"audio_id"` // 关联音频
ChannelID string `json:"channel_id"` // 关联通道
ChannelName string `json:"channel_name"` // 关联通道名称
AudioName string `json:"audio_name"` // 音频名称
Mode string `json:"mode"` // 文件类型 mp3 wav
Size int64 `json:"size"` // 文件大小
Duration int64 `json:"duration"` // 文件时长
}
type DelAudioTaskInput struct {
IDs []int `json:"ids"` // id
}

View File

@ -4,6 +4,7 @@ package audioencode
// Storer data
type Storer interface {
AudioEncode() AudioEncodeStorer
AudioTask() AudioTaskStorer
}
// Core

View File

@ -3,6 +3,7 @@ package audioencode
import (
"git.lnton.com/lnton/pkg/orm"
"time"
)
type EncodeStatus int
@ -23,6 +24,7 @@ type AudioEncode struct {
EncodeStatus EncodeStatus `gorm:"column:encode_status;notNull;default:0;comment:转码状态" json:"encode_status"` // 转码状态
Mode string `gorm:"column:mode;notNull;default:'';comment:文件类型" json:"mode"` // 文件类型 mp3 wav
Size int64 `gorm:"column:size;notNull;default:0;comment:文件大小" json:"size"` // 文件大小
Duration int64 `gorm:"column:duration;notNull;default:0;comment:文件时长" json:"duration"` // 文件时长
Des string `gorm:"column:des;notNull;default:'';comment:描述" json:"des"` // 描述
}
@ -30,3 +32,25 @@ type AudioEncode struct {
func (*AudioEncode) TableName() string {
return "audio_encode"
}
type AudioTask struct {
orm.Model
AudioID int `gorm:"column:audio_id;notNull;default:0;comment:关联音频" json:"audio_id"` // 关联音频
ChannelID string `gorm:"column:channel_id;notNull;default:'';comment:关联通道" json:"channel_id"` // 关联通道
ChannelName string `gorm:"column:channel_name;notNull;default:'';comment:关联通道名称" json:"channel_name"` // 关联通道名称
AudioName string `gorm:"column:audio_name;notNull;default:'';comment:音频名称" json:"audio_name"` // 音频名称
Mode string `gorm:"column:mode;notNull;default:'';comment:文件类型" json:"mode"` // 文件类型 mp3 wav
Size int64 `gorm:"column:size;notNull;default:0;comment:文件大小" json:"size"` // 文件大小
Duration int64 `gorm:"column:duration;notNull;default:0;comment:文件时长" json:"duration"` // 文件时长
TaskStatus int `gorm:"column:task_status;notNull;default:0;comment:任务状态" json:"task_status"` // 任务状态
CreateTime time.Time `gorm:"notNull;default:CURRENT_TIMESTAMP;index;comment:创建时间" json:"created_time"` // 任务创建时间
StartTime time.Time `gorm:"notNull;default:CURRENT_TIMESTAMP;index;comment:开始时间" json:"start_time"` // 任务开始时间
EndTime time.Time `gorm:"notNull;default:CURRENT_TIMESTAMP;index;comment:结束时间" json:"end_time"` // 任务结束时间
ErrorMsg string `gorm:"column:error_msg;notNull;default:'';comment:错误信息" json:"error_msg"` // 错误信息
}
// TableName database table name
func (*AudioTask) TableName() string {
return "audio_task"
}

View File

@ -48,9 +48,9 @@ func (d AudioEncode) Del(ctx context.Context, model *audioencode.AudioEncode, op
return orm.DeleteWithContext(ctx, d.db, model, opts...)
}
// EditEnable implements audioencode.AudioEncodeStorer.
func (d AudioEncode) EditEnable(enable bool, id int) error {
if err := d.db.Model(&audioencode.AudioEncode{}).Where(`id = ?`, id).Update("enable", enable).Error; err != nil {
// EditStatus implements audioencode.AudioEncodeStorer.
func (d AudioEncode) EditStatus(status audioencode.EncodeStatus, id int) error {
if err := d.db.Model(&audioencode.AudioEncode{}).Where(`id = ?`, id).Update("encode_status", status).Error; err != nil {
return err
}
return nil

View File

@ -0,0 +1,68 @@
// Code generated by gowebx, DO AVOID EDIT.
package audioencodedb
import (
"context"
"easyaudioencode/internal/core/audioencode"
"git.lnton.com/lnton/pkg/orm"
)
var _ audioencode.AudioTaskStorer = AudioTask{}
// AudioTask Related business namespaces
type AudioTask DB
// FindAll implements audioencode.AudioTaskStorer.
func (d AudioTask) FindAll(bs *[]*audioencode.AudioTask) (int64, error) {
db := d.db.Model(&audioencode.AudioTask{})
var total int64
if err := db.Count(&total).Error; err != nil || total <= 0 {
// 如果统计失败或者数量为0则返回错误
return 0, err
}
return total, db.Find(bs).Error
}
// Find implements audioencode.AudioTaskStorer.
func (d AudioTask) Find(ctx context.Context, bs *[]*audioencode.AudioTask, page orm.Pager, opts ...orm.QueryOption) (int64, error) {
return orm.FindWithContext(ctx, d.db, bs, page, opts...)
}
// Get implements audioencode.AudioTaskStorer.
func (d AudioTask) Get(ctx context.Context, model *audioencode.AudioTask, opts ...orm.QueryOption) error {
return orm.FirstWithContext(ctx, d.db, model, opts...)
}
// Add implements audioencode.AudioTaskStorer.
func (d AudioTask) Add(ctx context.Context, model *audioencode.AudioTask) error {
return d.db.WithContext(ctx).Create(model).Error
}
// Edit implements audioencode.AudioTaskStorer.
func (d AudioTask) Edit(ctx context.Context, model *audioencode.AudioTask, changeFn func(*audioencode.AudioTask), opts ...orm.QueryOption) error {
return orm.UpdateWithContext(ctx, d.db, model, changeFn, opts...)
}
// Del implements audioencode.AudioTaskStorer.
func (d AudioTask) Del(ctx context.Context, model *audioencode.AudioTask, opts ...orm.QueryOption) error {
return orm.DeleteWithContext(ctx, d.db, model, opts...)
}
// EditStatus implements audioencode.AudioTaskStorer.
func (d AudioTask) EditStatus(status int, id int) error {
if err := d.db.Model(&audioencode.AudioTask{}).Where(`id = ?`, id).Update("task_status", status).Error; err != nil {
return err
}
return nil
}
// EditStatusError implements audioencode.AudioTaskStorer.
func (d AudioTask) EditStatusError(id, status int, s string) error {
if err := d.db.Model(&audioencode.AudioTask{}).Where(`id = ?`, id).Updates(map[string]any{
"task_status": status,
"error_msg": s,
}).Error; err != nil {
return err
}
return nil
}

View File

@ -22,6 +22,9 @@ func NewDB(db *gorm.DB) DB {
func (d DB) AudioEncode() audioencode.AudioEncodeStorer {
return AudioEncode(d)
}
func (d DB) AudioTask() audioencode.AudioTaskStorer {
return AudioTask(d)
}
// AutoMigrate sync database
func (d DB) AutoMigrate(ok bool) DB {
@ -30,6 +33,7 @@ func (d DB) AutoMigrate(ok bool) DB {
}
if err := d.db.AutoMigrate(
new(audioencode.AudioEncode),
new(audioencode.AudioTask),
); err != nil {
panic(err)
}

View File

@ -57,6 +57,7 @@ func NewCore(cfg *conf.Bootstrap) *Core {
sdk.AddResponseHandler("findDevices", c.findDevicesRespH)
sdk.AddResponseHandler("findChannels", c.findChannelsRespH)
sdk.AddResponseHandler("getBaseConfig", c.getBaseConfigRespH)
sdk.AddResponseHandler("findTalkUrl", c.findTalkUrlRespH)
config, err := c.GetBaseConfig(context.TODO(), &ConfigBaseInput{Mode: "rtsp"})
if err != nil {
@ -91,3 +92,7 @@ func (c Core) getBaseConfigRespH(requestID string, args json.RawMessage) (interf
slog.Debug("Received 'getBaseConfig' from host", "request_id", requestID, "args", args)
return nil, nil
}
func (c Core) findTalkUrlRespH(requestID string, args json.RawMessage) (interface{}, error) {
slog.Debug("Received 'findTalkUrl' from host", "request_id", requestID, "args", args)
return nil, nil
}

View File

@ -0,0 +1,24 @@
package host
import (
"context"
"encoding/json"
)
func (c Core) FindTalkUrl(ctx context.Context, in *FindTalkInput) (*FinTalkOutput, error) {
marshal, err := json.Marshal(in)
if err != nil {
return nil, err
}
result, err := c.Plugin.CallHost("findTalkUrl", marshal)
if err != nil {
return nil, err
}
out := FinTalkOutput{}
if err = json.Unmarshal(result, &out); err != nil {
return nil, err
}
return &out, nil
}

View File

@ -0,0 +1,9 @@
package host
type FindTalkInput struct {
ChannelID string `form:"channel_id"`
}
type FinTalkOutput struct {
TalkUrl string `json:"talk_url"`
Transport string `json:"transport"`
}

View File

@ -0,0 +1,138 @@
package transcode
import (
"fmt"
"github.com/gorilla/websocket"
"log/slog"
"math/rand"
"net/url"
"os"
"path/filepath"
"time"
"github.com/pion/rtp"
)
func (tk *Task) StartTalkCode(wsURL string, transport string) error {
sampleRate := 8000
frameSize := 160
dir, _ := os.Getwd()
inputFile := filepath.Join(dir, tk.EncodeUrl)
//SSRC := tk.GetSSRCUint32()
if _, err := os.Stat(inputFile); os.IsNotExist(err) {
return fmt.Errorf("input file does not exist: %v", err.Error())
}
u, err := url.Parse(wsURL)
if err != nil {
return fmt.Errorf("invalid WebSocket URL: %v", err.Error())
}
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
return fmt.Errorf("failed to connect to WebSocket: %v", err.Error())
}
defer func() {
c.Close()
}()
// Open G711A file
file, err := os.Open(inputFile)
if err != nil {
return fmt.Errorf("failed to open input file: %v", err.Error())
}
defer file.Close()
// Initialize RTP parameters
seqNumber := uint16(0)
// Initialize with a random timestamp base as per RTP standards
initialTimestamp := uint32(time.Now().UnixNano() / 1e6) // Use current time as base
// Calculate frame duration in milliseconds
frameDuration := time.Duration(float64(frameSize)/float64(sampleRate)*1000) * time.Millisecond
nextSendTime := time.Now()
frame := make([]byte, frameSize)
var errors error
for {
if tk.isEnd {
break // End of file
}
n, err := file.Read(frame)
if err != nil {
if n == 0 {
break // End of file
}
slog.Error("Error reading file", "err", err.Error())
break
}
// Calculate current timestamp based on initial + frames sent * samples per frame
currentTimestamp := initialTimestamp + uint32(seqNumber)*uint32(frameSize)
// Create RTP packet for G711A
pkt := &rtp.Packet{
Header: rtp.Header{
Version: 2,
PayloadType: 8, // G711 A-law
SequenceNumber: seqNumber,
Timestamp: currentTimestamp,
SSRC: 123456,
},
Payload: frame[:n],
}
// Send RTP packet via WebSocket
data, err := pkt.Marshal()
if err != nil {
slog.Debug("Error marshaling RTP packet", "err", err.Error())
continue
}
if transport == "tcp" {
tcpBuf := make([]byte, 4)
tcpBuf[0] = byte(len(data) >> 24)
tcpBuf[1] = byte(len(data) >> 16)
tcpBuf[2] = byte(len(data) >> 8)
tcpBuf[3] = byte(len(data))
err = c.WriteMessage(websocket.BinaryMessage, tcpBuf)
if err != nil {
errors = fmt.Errorf("error sending RTP packet via WebSocket: %v", err.Error())
slog.Debug("Error sending RTP packet via WebSocket", "err", err.Error())
break
}
} else {
err = c.WriteMessage(websocket.BinaryMessage, data)
if err != nil {
errors = fmt.Errorf("error sending RTP packet via WebSocket: %v", err.Error())
slog.Debug("Error sending RTP packet via WebSocket", "err", err.Error())
break
}
}
// Increment sequence number
seqNumber++
// Control the sending rate to match real-time
// Wait until it's time to send the next frame
nextSendTime = nextSendTime.Add(frameDuration)
sleepDuration := time.Until(nextSendTime)
if sleepDuration > 0 {
time.Sleep(sleepDuration)
} else {
// If we're behind schedule, update nextSendTime to current time
nextSendTime = time.Now().Add(frameDuration)
}
}
if errors != nil {
return errors
}
slog.Info("G711A file sent successfully")
close(tk.success)
return nil
}
func (tk *Task) GetSSRCUint32() uint32 {
rand.Seed(time.Now().UnixNano())
// rand.Uint32() 会返回一个类型为 uint32 的值
// 其范围是 0 到 2^32 - 1 (即 0 到 4294967295)
return rand.Uint32()
}

View File

@ -1,35 +1,167 @@
package transcode
import (
"context"
"easyaudioencode/internal/core/audioencode"
"easyaudioencode/internal/core/host"
"sync"
)
type ChannelInfo struct {
}
var (
channelsMap sync.Map
//channelsMap = make(map[int]*ChannelInfo)
channelsLock sync.RWMutex
"easyaudioencode/pkg/ffmpeg"
"fmt"
"log/slog"
"strconv"
"time"
)
type Core struct {
HostCore *host.Core
AudioEncodeCore *audioencode.Core
WorkflowCore *Workflow
}
func NewCore(HostCore *host.Core, AudioEncodeCore *audioencode.Core) *Core {
core := &Core{
HostCore: HostCore,
AudioEncodeCore: AudioEncodeCore,
WorkflowCore: OpenAudioEncode(AudioEncodeCore),
}
// 启用任务管理器
return core
}
func (c Core) AddAudioEncode(pullUrl, pushUrl string, id int) error {
func OpenAudioEncode(AudioEncodeCore *audioencode.Core) *Workflow {
wf := NewWorkflow(WorkflowConfig{
MaxConcurrency: 100, // 并发
CleanupInterval: 30 * time.Second, // 每30秒清理一次
MaxTaskHistory: 500, // 最多保留500个任务历史
RetentionTime: 60 * time.Second, // 任务保留1分钟
})
// 设置回调函数
wf.SetCallbacks(
// 完成回调
func(task *Task) {
id, _ := strconv.Atoi(task.ID)
err := AudioEncodeCore.AudioTaskStatus(int(TaskCompleted), id)
if err != nil {
slog.Error("AudioTaskStatus", "status", TaskCompleted, "err", err.Error())
}
slog.Info(fmt.Sprintf("任务完成: ID=%s, Type=%s, 耗时=%v",
task.ID, task.Type, task.endTime.Sub(task.startTime)))
},
// 取消回调
func(task *Task) {
id, _ := strconv.Atoi(task.ID)
err := AudioEncodeCore.AudioTaskStatus(int(TaskCancelled), id)
if err != nil {
slog.Error("AudioTaskStatus", "status", TaskCancelled, "err", err.Error())
}
task.isEnd = true
slog.Info(fmt.Sprintf("任务取消: ID=%s, Type=%s", task.ID, task.Type))
},
// 错误回调
func(task *Task, errs error) {
id, _ := strconv.Atoi(task.ID)
err := AudioEncodeCore.AudioTaskStatusError(id, int(TaskFailed), errs.Error())
if err != nil {
slog.Error("AudioTaskStatus", "status", TaskFailed, "err", err.Error())
}
task.isEnd = true
slog.Info(fmt.Sprintf("任务失败: ID=%s, Type=%s, 错误=%v",
task.ID, task.Type, err))
},
// 清理回调
func(task *Task, reason string) {
slog.Info(fmt.Sprintf("任务清理: ID=%s, 原因=%s", task.ID, reason))
},
)
return wf
}
func (c Core) WorkflowExecute(ctx context.Context, t *Task) error {
slog.Info(fmt.Sprintf("开始执行任务: %s (类型: %s)", t.ID, t.Type))
if c.WorkflowCore.GetIsTasks(t.Type) {
time.Sleep(11 * time.Second)
}
//return fmt.Errorf("请求失败")
in := &host.FindTalkInput{
ChannelID: t.Type,
}
info, err := c.HostCore.FindTalkUrl(ctx, in)
if err != nil {
return fmt.Errorf("FindTalkUrl: %v", err.Error())
}
slog.Info(fmt.Sprintf("开始执行任务: URL:[%s] 类型:[%s]", info.TalkUrl, info.Transport))
go func(in *host.FinTalkOutput) {
t.quit <- t.StartTalkCode(in.TalkUrl, in.Transport)
}(info)
select {
case errs := <-t.quit:
slog.Info("处理异常退出", "id", t.ID)
return errs
case <-t.success:
slog.Info("处理完成", "id", t.ID)
return nil
// 用于测试
//case <-time.After(30 * time.Second):
// slog.Info("任务完成", "id", t.ID)
// t.isEnd = true
// return nil
case <-ctx.Done():
slog.Error("任务中断", "id", t.ID)
t.isEnd = true
return ctx.Err()
}
}
func (c Core) AddTask(id int, bid string, EncodeUrl string, duration int64) error {
newTask := &Task{
ID: strconv.Itoa(id),
Type: bid,
EncodeUrl: EncodeUrl,
Duration: duration,
quit: make(chan error, 3),
success: make(chan struct{}),
isEnd: false,
execute: c.WorkflowExecute,
}
err := c.WorkflowCore.AddTask(newTask)
if err != nil {
slog.Error("添加任务失败", "id", id, "err", err.Error())
return fmt.Errorf("添加任务失败 【%d】%v", id, err.Error())
}
slog.Info("添加任务成功", "id", id)
return nil
}
func (c Core) CancelTask(id int) error {
if c.WorkflowCore.CancelTask(strconv.Itoa(id)) {
slog.Info("已取消任务", "id", id)
return nil
}
slog.Warn("取消的任务不存在或者已被取消", "id", id)
return fmt.Errorf("取消的任务不存在或者已被取消 %d", id)
}
func (c Core) GetTaskInfo(id int) (*TaskInfo, error) {
task, t := c.WorkflowCore.GetTaskInfo(strconv.Itoa(id))
if !t {
slog.Debug("查询任务详情失败", "id", id)
return &TaskInfo{}, fmt.Errorf("查询任务详情失败 %d", id)
}
return task, nil
}
// StartAudioEncode 转码
func (c Core) StartAudioEncode(inputFile, outputFile string, id int) {
go func() {
status := audioencode.EncodeStatusSuccess
err := ffmpeg.TranscodeToG711AFile(inputFile, outputFile)
if err != nil {
slog.Error("StartAudioEncode TranscodeToG711AFile", "err", err.Error())
status = audioencode.EncodeStatusFailed
}
err = c.AudioEncodeCore.AudioEncodeStatus(status, id)
if err != nil {
slog.Error("StartAudioEncode AudioEncodeStatus", "err", err.Error())
}
}()
return
}

View File

@ -0,0 +1,680 @@
package transcode
import (
"context"
"errors"
"fmt"
"log"
"runtime/debug"
"sync"
"time"
)
// Task 任务定义
type Task struct {
ID string // 任务唯一ID
Type string // 任务类型
EncodeUrl string // 任务音频地址
execute func(ctx context.Context, task *Task) error // 执行函数
ctx context.Context // 任务上下文
cancelFunc context.CancelFunc // 取消函数
status TaskStatus // 任务状态
startTime time.Time // 开始时间
endTime time.Time // 结束时间
createTime time.Time // 创建时间
quit chan error
success chan struct{}
isEnd bool
Duration int64
}
type TaskInfo struct {
Status TaskStatus // 任务状态
StartTime time.Time // 开始时间
EndTime time.Time // 结束时间
CreateTime time.Time // 创建时间
}
// TaskStatus 任务状态
type TaskStatus int
const (
TaskPending TaskStatus = iota // 等待中
TaskRunning // 执行中
TaskCompleted // 已完成
TaskCancelled // 已取消
TaskFailed // 已失败
)
// Workflow 工作流
type Workflow struct {
maxConcurrency int // 最大并发数
semaphore chan struct{} // 并发信号量
tasks map[string]*Task // 所有任务
taskQueue map[string][]string // 按类型分组的任务队列
runningTasks map[string]string // 正在运行的任务类型 -> 任务ID
mu sync.RWMutex // 读写锁
wg sync.WaitGroup // 等待组
cleanupTicker *time.Ticker // 清理定时器
cleanupStop chan struct{} // 清理停止信号
// 清理配置
cleanupInterval time.Duration // 清理间隔
maxTaskHistory int // 最大历史任务数
retentionTime time.Duration // 任务保留时间
// 回调函数
onComplete func(task *Task) // 完成回调
onCancel func(task *Task) // 取消回调
onError func(task *Task, err error) // 错误回调
onCleanup func(task *Task, reason string) // 清理回调
}
// WorkflowConfig 工作流配置
type WorkflowConfig struct {
MaxConcurrency int // 最大并发数默认2
CleanupInterval time.Duration // 清理间隔默认30秒
MaxTaskHistory int // 最大历史任务数默认1000
RetentionTime time.Duration // 任务保留时间默认5分钟
}
// NewWorkflow 创建工作流
func NewWorkflow(config WorkflowConfig) *Workflow {
if config.MaxConcurrency <= 0 {
config.MaxConcurrency = 2
}
if config.CleanupInterval <= 0 {
config.CleanupInterval = 30 * time.Second
}
if config.MaxTaskHistory <= 0 {
config.MaxTaskHistory = 1000
}
if config.RetentionTime <= 0 {
config.RetentionTime = 5 * time.Minute
}
wf := &Workflow{
maxConcurrency: config.MaxConcurrency,
semaphore: make(chan struct{}, config.MaxConcurrency),
tasks: make(map[string]*Task),
taskQueue: make(map[string][]string),
runningTasks: make(map[string]string),
cleanupInterval: config.CleanupInterval,
maxTaskHistory: config.MaxTaskHistory,
retentionTime: config.RetentionTime,
cleanupStop: make(chan struct{}),
}
// 启动清理协程
wf.startCleanupRoutine()
return wf
}
// SetCallbacks 设置回调函数
func (wf *Workflow) SetCallbacks(
onComplete func(task *Task),
onCancel func(task *Task),
onError func(task *Task, err error),
onCleanup func(task *Task, reason string),
) {
wf.mu.Lock()
defer wf.mu.Unlock()
wf.onComplete = onComplete
wf.onCancel = onCancel
wf.onError = onError
wf.onCleanup = onCleanup
}
// AddTask 添加新任务
func (wf *Workflow) AddTask(task *Task) error {
if task == nil {
return errors.New("task cannot be nil")
}
if task.ID == "" {
return errors.New("task ID cannot be empty")
}
if task.execute == nil {
return errors.New("task execute function cannot be nil")
}
wf.mu.Lock()
defer wf.mu.Unlock()
// 检查任务是否已存在
if _, exists := wf.tasks[task.ID]; exists {
return fmt.Errorf("task with ID %s already exists", task.ID)
}
// 设置任务初始状态
task.status = TaskPending
task.ctx, task.cancelFunc = context.WithCancel(context.Background())
task.createTime = time.Now()
// 保存任务到map
wf.tasks[task.ID] = task
// 添加到类型队列
if _, ok := wf.taskQueue[task.Type]; !ok {
wf.taskQueue[task.Type] = make([]string, 0)
}
wf.taskQueue[task.Type] = append(wf.taskQueue[task.Type], task.ID)
// 尝试执行任务(在锁外执行,避免死锁)
go func() {
wf.tryExecuteTask(task.Type)
}()
return nil
}
// tryExecuteTask 尝试执行任务
func (wf *Workflow) tryExecuteTask(taskType string) {
wf.mu.Lock()
// 检查是否有该类型的任务正在运行
if _, isRunning := wf.runningTasks[taskType]; isRunning {
wf.mu.Unlock()
return
}
// 获取该类型的任务队列
queue, exists := wf.taskQueue[taskType]
if !exists || len(queue) == 0 {
wf.mu.Unlock()
return
}
// 获取队列中的第一个任务
taskID := queue[0]
task, taskExists := wf.tasks[taskID]
if !taskExists || task.status != TaskPending {
wf.mu.Unlock()
return
}
wf.mu.Unlock()
// 尝试获取并发许可(非阻塞)
select {
case wf.semaphore <- struct{}{}:
// 获取成功,可以执行
wf.executeTask(task)
default:
// 并发已达上限,等待下次尝试
}
}
// executeTask 执行任务
func (wf *Workflow) executeTask(task *Task) {
wf.mu.Lock()
// 再次检查任务状态
if task.status != TaskPending {
wf.mu.Unlock()
<-wf.semaphore // 释放信号量
return
}
// 更新任务状态
task.status = TaskRunning
task.startTime = time.Now()
// 记录该类型任务正在运行
wf.runningTasks[task.Type] = task.ID
// 从队列中移除(第一个元素)
if queue, exists := wf.taskQueue[task.Type]; exists && len(queue) > 0 {
wf.taskQueue[task.Type] = queue[1:]
}
wf.mu.Unlock()
wf.wg.Add(1)
// 启动任务执行
go func() {
defer func() {
// 恢复panic
if r := recover(); r != nil {
err := fmt.Errorf("task panic: %v\n%s", r, debug.Stack())
wf.handleTaskError(task, err)
}
wf.taskCompleted(task.Type)
wf.wg.Done()
}()
// 设置超时
timeNum := 12 * 60 * time.Minute
if task.Duration > 0 {
timeNum = time.Duration(task.Duration) * time.Second
}
ctx, cancel := context.WithTimeout(task.ctx, timeNum)
defer cancel()
// 执行任务
err := task.execute(ctx, task)
// 处理任务结果
wf.mu.Lock()
defer wf.mu.Unlock()
if err != nil {
if errors.Is(err, context.Canceled) {
task.status = TaskCancelled
task.endTime = time.Now()
wf.triggerOnCancel(task)
} else {
task.status = TaskFailed
task.endTime = time.Now()
wf.triggerOnError(task, err)
}
} else {
task.status = TaskCompleted
task.endTime = time.Now()
wf.triggerOnComplete(task)
}
// 释放该类型的运行标记
delete(wf.runningTasks, task.Type)
}()
}
// taskCompleted 任务完成处理
func (wf *Workflow) taskCompleted(taskType string) {
// 释放并发许可
<-wf.semaphore
// 尝试执行下一个同类型任务
go wf.tryExecuteTask(taskType)
// 检查是否有其他类型的任务可以执行
wf.mu.RLock()
allTypes := make([]string, 0, len(wf.taskQueue))
for t := range wf.taskQueue {
allTypes = append(allTypes, t)
}
wf.mu.RUnlock()
for _, t := range allTypes {
if t != taskType {
go wf.tryExecuteTask(t)
}
}
}
// CancelTask 取消任务
func (wf *Workflow) CancelTask(taskID string) bool {
wf.mu.Lock()
defer wf.mu.Unlock()
task, exists := wf.tasks[taskID]
if !exists {
return false
}
switch task.status {
case TaskPending:
// 从队列中移除
if queue, exists := wf.taskQueue[task.Type]; exists {
newQueue := make([]string, 0, len(queue))
for _, id := range queue {
if id != taskID {
newQueue = append(newQueue, id)
}
}
wf.taskQueue[task.Type] = newQueue
}
task.status = TaskCancelled
task.endTime = time.Now()
wf.triggerOnCancel(task)
return true
case TaskRunning:
// 取消正在执行的任务
if task.cancelFunc != nil {
task.cancelFunc()
}
return true
default:
// 已完成或已取消的任务
return false
}
}
// CleanupCompletedTasks 清理已完成的任务
func (wf *Workflow) CleanupCompletedTasks() int {
wf.mu.Lock()
defer wf.mu.Unlock()
return wf.cleanupTasksInternal("manual", false)
}
// CleanupOldTasks 清理旧任务(基于保留时间)
func (wf *Workflow) CleanupOldTasks() int {
wf.mu.Lock()
defer wf.mu.Unlock()
return wf.cleanupTasksInternal("retention", false)
}
// CleanupAllTasks 清理所有任务(谨慎使用)
func (wf *Workflow) CleanupAllTasks(force bool) int {
wf.mu.Lock()
defer wf.mu.Unlock()
return wf.cleanupTasksInternal("all", force)
}
// cleanupTasksInternal 内部清理方法
func (wf *Workflow) cleanupTasksInternal(reason string, force bool) int {
cleanedCount := 0
now := time.Now()
for id, task := range wf.tasks {
// 跳过运行中和等待中的任务(除非强制清理)
if !force && (task.status == TaskRunning || task.status == TaskPending) {
continue
}
// 检查是否应该清理
shouldClean := false
switch reason {
case "manual":
// 手动清理:只清理已结束的任务
shouldClean = task.status == TaskCompleted ||
task.status == TaskCancelled ||
task.status == TaskFailed
case "retention":
// 基于保留时间清理
if task.endTime.IsZero() {
// 如果没有结束时间,使用创建时间
shouldClean = now.Sub(task.createTime) > wf.retentionTime
} else {
shouldClean = now.Sub(task.endTime) > wf.retentionTime
}
case "all":
// 清理所有任务(包括运行中和等待中的)
shouldClean = true
default:
// 默认清理已结束的任务
shouldClean = task.status == TaskCompleted ||
task.status == TaskCancelled ||
task.status == TaskFailed
}
if shouldClean {
// 触发清理回调
if wf.onCleanup != nil {
wf.onCleanup(task, reason)
}
// 从tasks map中删除
delete(wf.tasks, id)
cleanedCount++
}
}
// 清理空的任务队列
for taskType, queue := range wf.taskQueue {
newQueue := make([]string, 0, len(queue))
for _, taskID := range queue {
if _, exists := wf.tasks[taskID]; exists {
newQueue = append(newQueue, taskID)
}
}
wf.taskQueue[taskType] = newQueue
// 如果队列为空,删除该类型
if len(newQueue) == 0 {
delete(wf.taskQueue, taskType)
}
}
// 清理运行任务记录
for taskType, taskID := range wf.runningTasks {
if _, exists := wf.tasks[taskID]; !exists {
delete(wf.runningTasks, taskType)
}
}
return cleanedCount
}
// startCleanupRoutine 启动清理协程
func (wf *Workflow) startCleanupRoutine() {
wf.cleanupTicker = time.NewTicker(wf.cleanupInterval)
go func() {
for {
select {
case <-wf.cleanupTicker.C:
// 定期清理旧任务
wf.mu.Lock()
cleaned := wf.cleanupTasksInternal("retention", false)
wf.mu.Unlock()
if cleaned > 0 {
log.Printf("自动清理了 %d 个旧任务", cleaned)
}
// 检查任务数量,如果超过限制,清理最旧的任务
wf.mu.Lock()
if len(wf.tasks) > wf.maxTaskHistory {
wf.cleanupExcessTasks()
}
wf.mu.Unlock()
case <-wf.cleanupStop:
wf.cleanupTicker.Stop()
return
}
}
}()
}
// cleanupExcessTasks 清理超出限制的任务
func (wf *Workflow) cleanupExcessTasks() {
if len(wf.tasks) <= wf.maxTaskHistory {
return
}
// 收集所有任务并按创建时间排序
tasks := make([]*Task, 0, len(wf.tasks))
for _, task := range wf.tasks {
tasks = append(tasks, task)
}
// 按创建时间排序(最旧的在前面)
for i := 0; i < len(tasks); i++ {
for j := i + 1; j < len(tasks); j++ {
if tasks[j].createTime.Before(tasks[i].createTime) {
tasks[i], tasks[j] = tasks[j], tasks[i]
}
}
}
// 计算需要清理的数量
excess := len(tasks) - wf.maxTaskHistory
// 清理最旧的任务(跳过运行中和等待中的任务)
cleaned := 0
for i := 0; i < len(tasks) && cleaned < excess; i++ {
task := tasks[i]
// 只清理已结束的任务
if task.status == TaskCompleted || task.status == TaskCancelled || task.status == TaskFailed {
if wf.onCleanup != nil {
wf.onCleanup(task, "max_history")
}
delete(wf.tasks, task.ID)
cleaned++
}
}
if cleaned > 0 {
log.Printf("清理了 %d 个任务以保持历史记录不超过 %d", cleaned, wf.maxTaskHistory)
}
}
// GetTaskStatus 获取任务状态
func (wf *Workflow) GetTaskStatus(taskID string) (TaskStatus, bool) {
wf.mu.RLock()
defer wf.mu.RUnlock()
task, exists := wf.tasks[taskID]
if !exists {
return TaskPending, false
}
return task.status, true
}
// GetTaskInfo 获取任务详情
func (wf *Workflow) GetTaskInfo(taskID string) (*TaskInfo, bool) {
wf.mu.RLock()
defer wf.mu.RUnlock()
task, exists := wf.tasks[taskID]
if !exists {
return &TaskInfo{}, false
}
return &TaskInfo{
Status: task.status,
EndTime: task.endTime,
CreateTime: task.createTime,
StartTime: task.startTime,
}, true
}
// GetRunningTasks 获取正在运行的任务
func (wf *Workflow) GetRunningTasks() []*Task {
wf.mu.RLock()
defer wf.mu.RUnlock()
result := make([]*Task, 0)
for _, task := range wf.tasks {
if task.status == TaskRunning {
result = append(result, task)
}
}
return result
}
// GetPendingTasks 获取等待中的任务
func (wf *Workflow) GetPendingTasks() []*Task {
wf.mu.RLock()
defer wf.mu.RUnlock()
result := make([]*Task, 0)
for _, task := range wf.tasks {
if task.status == TaskPending {
result = append(result, task)
}
}
return result
}
// GetPendingTasks 获取等待中的任务
func (wf *Workflow) GetIsTasks(bid string) bool {
wf.mu.RLock()
defer wf.mu.RUnlock()
result := false
for _, task := range wf.tasks {
if task.Type == bid && task.status != TaskPending && task.status != TaskRunning {
result = true
break
}
}
return result
}
// GetTaskCount 获取各种状态的任务数量
func (wf *Workflow) GetTaskCount() (total, running, pending, completed, cancelled, failed int) {
wf.mu.RLock()
defer wf.mu.RUnlock()
total = len(wf.tasks)
for _, task := range wf.tasks {
switch task.status {
case TaskRunning:
running++
case TaskPending:
pending++
case TaskCompleted:
completed++
case TaskCancelled:
cancelled++
case TaskFailed:
failed++
}
}
return
}
// Wait 等待所有任务完成
func (wf *Workflow) Wait() {
wf.wg.Wait()
}
// Stop 停止工作流
func (wf *Workflow) Stop() {
wf.mu.Lock()
defer wf.mu.Unlock()
// 发送清理停止信号
close(wf.cleanupStop)
// 取消所有任务
for _, task := range wf.tasks {
if task.status == TaskPending || task.status == TaskRunning {
if task.cancelFunc != nil {
task.cancelFunc()
}
}
}
}
// 触发回调函数
func (wf *Workflow) triggerOnComplete(task *Task) {
if wf.onComplete != nil {
wf.onComplete(task)
}
}
func (wf *Workflow) triggerOnCancel(task *Task) {
if wf.onCancel != nil {
wf.onCancel(task)
}
}
func (wf *Workflow) triggerOnError(task *Task, err error) {
if wf.onError != nil {
wf.onError(task, err)
}
}
func (wf *Workflow) handleTaskError(task *Task, err error) {
wf.mu.Lock()
defer wf.mu.Unlock()
task.status = TaskFailed
task.endTime = time.Now()
wf.triggerOnError(task, err)
delete(wf.runningTasks, task.Type)
}

View File

@ -75,6 +75,7 @@ func setupRouter(r *gin.Engine, uc *Usecase) {
ctx.Redirect(http.StatusTemporaryRedirect, target)
return
}
if strings.HasPrefix(p, "/extensions/easyaudioencode") {
// 改为前缀替换并在当前请求内重新分发,而不是重定向
newPath := strings.TrimPrefix(p, "/extensions/easyaudioencode")

View File

@ -11,8 +11,8 @@ import (
"git.lnton.com/lnton/pkg/reason"
"git.lnton.com/lnton/pkg/web"
"github.com/gin-gonic/gin"
"os"
"strconv"
"time"
)
type Loger interface {
@ -41,60 +41,30 @@ func RegisterAudioEncode(g gin.IRouter, api AudioEncodeAPI, handler ...gin.Handl
group.DELETE("/:id", web.WarpH(api.delAudioEncode))
group.POST("/upload", web.WarpH(api.uploadAudioHandler))
group.GET("/:id/start", web.WarpH(api.getStart))
group.GET("/:id/stop", web.WarpH(api.getStop))
group.GET("/:id/snap", web.WarpH(api.getSnap))
group.GET("/:id/code", web.WarpH(api.getCode))
// 任务
groupTask := g.Group("/api/audiotask", handler...)
groupTask.POST("", web.WarpH(api.addAudioTask))
groupTask.GET("", web.WarpH(api.findAudioTask))
groupTask.DELETE("/:id", web.WarpH(api.delAudioTask))
groupTask.DELETE("", web.WarpH(api.delAudioTaskAll))
groupTask.GET("/cancel/:id", web.WarpH(api.cancelAudioTask))
}
}
// >>> pulltopushServer >>>>>>>>>>>>>>>>>>>>
func (a AudioEncodeAPI) getStart(c *gin.Context, _ *struct{}) (any, error) {
// >>> transcodeCore >>>>>>>>>>>>>>>>>>>>
func (a AudioEncodeAPI) getCode(c *gin.Context, _ *struct{}) (any, error) {
ID, _ := strconv.Atoi(c.Param("id"))
_, err := a.core.GetAudioEncode(c.Request.Context(), ID)
info, err := a.core.GetAudioEncode(c.Request.Context(), ID)
if err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`find audioencode [%d] err [%s]`, ID, err.Error()))
}
//err = a.sourceCore.StartStream(info.ID)
//if err != nil {
// return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`StartStream [%d] err [%s]`, ID, err.Error()))
//}
err = a.core.AudioEncodeEnable(true, ID)
if err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`AudioEncodeEnable [%d] err [%s]`, ID, err.Error()))
}
a.transcodeCore.StartAudioEncode(info.SourceUrl, info.EncodeUrl, info.ID)
return gin.H{"data": "OK!"}, err
}
func (a AudioEncodeAPI) getStop(c *gin.Context, _ *struct{}) (any, error) {
ID, _ := strconv.Atoi(c.Param("id"))
_, err := a.core.GetAudioEncode(c.Request.Context(), ID)
if err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`find audioencode [%d] err [%s]`, ID, err.Error()))
}
//err = a.sourceCore.StopStream(info.ID)
//if err != nil {
// return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`StopStream [%d] err [%s]`, ID, err.Error()))
//}
err = a.core.AudioEncodeEnable(false, ID)
if err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`AudioEncodeEnable [%d] err [%s]`, ID, err.Error()))
}
return gin.H{"data": "OK!"}, err
}
func (a AudioEncodeAPI) getSnap(c *gin.Context, _ *struct{}) (any, error) {
ID, _ := strconv.Atoi(c.Param("id"))
_, err := a.core.GetAudioEncode(c.Request.Context(), ID)
if err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`find audioencode [%d] err [%s]`, ID, err.Error()))
}
//_ = a.sourceCore.SnapStream(info.ID)
time.Sleep(200 * time.Microsecond)
return gin.H{"data": fmt.Sprintf("/snap_%d.jpg", ID)}, err
}
func (a AudioEncodeAPI) findAudioEncode(c *gin.Context, in *audioencode.FindAudioEncodeInput) (any, error) {
items, total, err := a.core.FindAudioEncode(c.Request.Context(), in)
rows := make([]map[string]interface{}, 0)
@ -112,6 +82,7 @@ func (a AudioEncodeAPI) findAudioEncode(c *gin.Context, in *audioencode.FindAudi
row["created_at"] = item.CreatedAt
row["updated_at"] = item.UpdatedAt
row["encode_status"] = item.EncodeStatus
row["duration"] = item.Duration
row["des"] = item.Des
rows = append(rows, row)
}
@ -136,7 +107,7 @@ func (a AudioEncodeAPI) getAudioEncode(c *gin.Context, _ *struct{}) (any, error)
row["created_at"] = item.CreatedAt
row["updated_at"] = item.UpdatedAt
row["encode_status"] = item.EncodeStatus
row["duration"] = item.Duration
row["des"] = item.Des
return gin.H{"data": row}, err
}
@ -167,9 +138,18 @@ func (a AudioEncodeAPI) addAudioEncode(c *gin.Context, in *audioencode.AddAudioE
func (a AudioEncodeAPI) delAudioEncode(c *gin.Context, _ *struct{}) (any, error) {
ID, _ := strconv.Atoi(c.Param("id"))
_, err := a.core.DelAudioEncode(c.Request.Context(), ID)
info, err := a.core.DelAudioEncode(c.Request.Context(), ID)
if err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del audioencode [%d] err [%s]`, ID, err.Error()))
}
err = os.Remove(info.SourceUrl)
if err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del SourceUrl [%d] err [%s]`, ID, err.Error()))
}
err = os.Remove(info.EncodeUrl)
if err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del SourceUrl [%d] err [%s]`, ID, err.Error()))
}
return gin.H{"data": "OK!"}, err
}

View File

@ -0,0 +1,131 @@
package api
import (
"easyaudioencode/internal/core/audioencode"
"fmt"
"git.lnton.com/lnton/pkg/reason"
"github.com/gin-gonic/gin"
"strconv"
)
func (a AudioEncodeAPI) findAudioTask(c *gin.Context, in *audioencode.FindAudioTaskInput) (any, error) {
items, total, err := a.core.FindAudioTask(c.Request.Context(), in)
rows := make([]map[string]interface{}, 0)
for _, item := range items {
//row := structs.Map(item)
row := make(map[string]interface{})
row["audio_id"] = item.AudioID
row["audio_name"] = item.AudioName
row["channel_id"] = item.ChannelID
row["channel_name"] = item.ChannelName
row["size"] = item.Size
row["duration"] = item.Duration
row["id"] = item.ID
row["mode"] = item.Mode
row["created_at"] = item.CreatedAt
row["updated_at"] = item.UpdatedAt
row["task_status"] = item.TaskStatus
row["error_msg"] = item.ErrorMsg
task, errs := a.transcodeCore.GetTaskInfo(item.ID)
if errs == nil {
row["task_status"] = task.Status
row["end_time"] = task.EndTime
row["create_time"] = task.CreateTime
row["start_time"] = task.StartTime
} else {
if item.TaskStatus == 1 || item.TaskStatus == 0 {
row["task_status"] = 2
}
}
rows = append(rows, row)
}
return gin.H{"items": rows, "total": total}, err
}
func (a AudioEncodeAPI) addAudioTask(c *gin.Context, in *audioencode.AddAudioTaskInput) (any, error) {
if len(in.Items) == 0 {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`add audioencode Items is empty`))
}
var outputList []audioencode.AddErrorOutput
for _, item := range in.Items {
audioInfo, err := a.core.GetAudioEncode(c.Request.Context(), item.AudioID)
if err != nil {
list := audioencode.AddErrorOutput{
AudioID: item.AudioID,
ChannelID: item.ChannelID,
ErrorMsg: fmt.Sprintf(`find audioencode [%d] err [%s]`, item.AudioID, err.Error()),
ChannelName: item.ChannelName,
}
outputList = append(outputList, list)
continue
}
inAdd := &audioencode.AddAudioTask{
AudioID: item.AudioID,
ChannelID: item.ChannelID,
ChannelName: item.ChannelName,
Size: audioInfo.Size,
Duration: audioInfo.Duration,
AudioName: audioInfo.Name,
Mode: audioInfo.Mode,
}
info, err := a.core.AddAudioTask(c.Request.Context(), inAdd)
if err != nil {
list := audioencode.AddErrorOutput{
AudioID: item.AudioID,
ChannelID: item.ChannelID,
ErrorMsg: fmt.Sprintf(`find audioencode [%d] err [%s]`, item.AudioID, err.Error()),
ChannelName: item.ChannelName,
}
outputList = append(outputList, list)
continue
}
err = a.transcodeCore.AddTask(info.ID, info.ChannelID, audioInfo.EncodeUrl, audioInfo.Duration)
if err != nil {
list := audioencode.AddErrorOutput{
AudioID: item.AudioID,
ChannelID: item.ChannelID,
ErrorMsg: fmt.Sprintf(`find audioencode [%d] err [%s]`, item.AudioID, err.Error()),
ChannelName: item.ChannelName,
}
outputList = append(outputList, list)
continue
}
}
if len(outputList) == 0 {
return gin.H{"data": "ok", "msg": "下发成功"}, nil
}
return gin.H{"data": outputList, "msg": "下发失败通道!"}, nil
}
func (a AudioEncodeAPI) cancelAudioTask(c *gin.Context, _ *struct{}) (any, error) {
ID, _ := strconv.Atoi(c.Param("id"))
err := a.transcodeCore.CancelTask(ID)
if err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`add CancelTask err [%s]`, err.Error()))
}
return gin.H{"data": "OK!"}, err
}
func (a AudioEncodeAPI) delAudioTask(c *gin.Context, _ *struct{}) (any, error) {
ID, _ := strconv.Atoi(c.Param("id"))
_, err := a.core.DelAudioTask(c.Request.Context(), ID)
if err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del audioencode [%d] err [%s]`, ID, err.Error()))
}
return gin.H{"data": "OK!"}, err
}
func (a AudioEncodeAPI) delAudioTaskAll(c *gin.Context, in *audioencode.DelAudioTaskInput) (any, error) {
if len(in.IDs) == 0 {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del audioencode ids is empty`))
}
_, err := a.core.DelAudioTaskAll(c.Request.Context(), in.IDs)
if err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del audioencode [%d] err [%s]`, in.IDs, err.Error()))
}
return gin.H{"data": "OK!"}, err
}

View File

@ -2,6 +2,7 @@ package api
import (
"easyaudioencode/internal/core/audioencode"
"easyaudioencode/pkg/ffmpeg"
"fmt"
"git.lnton.com/lnton/pkg/reason"
"github.com/gin-gonic/gin"
@ -44,6 +45,9 @@ func (a AudioEncodeAPI) uploadAudioHandler(c *gin.Context, _ *struct{}) (any, er
if err := os.MkdirAll(filepath.Join(uploadDir, sourceDir), 0755); err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf("创建上传目录失败: %v", err.Error()))
}
if err := os.MkdirAll(filepath.Join(uploadDir, encodeDir), 0755); err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf("创建转码目录失败: %v", err.Error()))
}
// 限制请求体大小
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxUploadSize)
if err := c.Request.ParseMultipartForm(maxUploadSize); err != nil {
@ -76,7 +80,7 @@ func (a AudioEncodeAPI) uploadAudioHandler(c *gin.Context, _ *struct{}) (any, er
// 生成唯一文件名(避免覆盖)
fileName := fmt.Sprintf("%s%s", uuidStr, fileExt)
filePath := filepath.Join(uploadDir, sourceDir, fileName)
outputFile := filepath.Join(uploadDir, encodeDir, fmt.Sprintf("%s.g711a", uuidStr))
// 创建目标文件
dstFile, err := os.Create(filePath)
if err != nil {
@ -88,18 +92,25 @@ func (a AudioEncodeAPI) uploadAudioHandler(c *gin.Context, _ *struct{}) (any, er
if _, err := io.Copy(dstFile, file); err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf("保存文件失败: %v", err.Error()))
}
in := &audioencode.AddAudioEncodeInput{
Name: sourceFileName,
FileName: fileName,
Size: fileHeader.Size,
SourceUrl: filePath,
Mode: fileExt,
Name: sourceFileName,
FileName: fileName,
Size: fileHeader.Size,
SourceUrl: filePath,
EncodeUrl: outputFile,
Mode: fileExt,
EncodeStatus: audioencode.EncodeStatusPing,
}
_, err = a.core.AddAudioEncode(c.Request.Context(), in)
duration, _, err := ffmpeg.GetAudioDuration(filePath)
if err == nil {
in.Duration = int64(duration.Seconds())
}
info, err := a.core.AddAudioEncode(c.Request.Context(), in)
if err != nil {
return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`add audioencode err [%s]`, err.Error()))
}
a.transcodeCore.StartAudioEncode(filePath, outputFile, info.ID)
return gin.H{"data": "上传成功!", "filePath": filePath, "filename": fileName, "size": fileHeader.Size}, err
}

197
pkg/ffmpeg/core.go Normal file
View File

@ -0,0 +1,197 @@
package ffmpeg
import (
"fmt"
"github.com/tcolgate/mp3"
"github.com/youpy/go-wav"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
)
// 音频转码配置
type TranscodeConfig struct {
InputPath string // 输入文件路径MP3/WAV
OutputPath string // 输出文件路径G711A
SampleRate int // 采样率(默认 8000 HzG711A 标准采样率)
Channels int // 声道数(默认 1单声道
}
// 默认转码配置
func defaultTranscodeConfig(input, output string) TranscodeConfig {
return TranscodeConfig{
InputPath: input,
OutputPath: output,
SampleRate: 8000, // G711A 标准采样率
Channels: 1, // 单声道(电话/语音常用)
}
}
// 校验输入文件类型(仅允许 MP3/WAV
func validateInputFile(inputPath string) error {
// 检查文件是否存在
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
return fmt.Errorf("输入文件不存在: %s", inputPath)
}
// 校验文件扩展名
ext := strings.ToLower(filepath.Ext(inputPath))
if ext != ".mp3" && ext != ".wav" {
return fmt.Errorf("仅支持 MP3/WAV 格式,当前文件扩展名: %s", ext)
}
return nil
}
// MP3/WAV 转 G711APCMA
func TranscodeToG711A(config TranscodeConfig) error {
// 1. 校验输入文件
if err := validateInputFile(config.InputPath); err != nil {
return err
}
// 2. 补全默认配置
if config.SampleRate <= 0 {
config.SampleRate = 8000
}
if config.Channels <= 0 {
config.Channels = 1
}
// 3. 确保输出目录存在
outputDir := filepath.Dir(config.OutputPath)
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("创建输出目录失败: %v", err)
}
// 4. 构建 FFmpeg 命令
// 核心参数说明:
// -i: 输入文件
// -ar: 采样率
// -ac: 声道数
// -f: 输出格式alaw 即 G711A
// -y: 覆盖已存在的输出文件
cmdArgs := []string{
"-i", config.InputPath,
"-ar", fmt.Sprintf("%d", config.SampleRate),
"-ac", fmt.Sprintf("%d", config.Channels),
"-f", "alaw", // 指定输出格式为 G711APCMA
"-y", // 覆盖输出文件(无需确认)
config.OutputPath,
}
dir, _ := os.Getwd() // Windows 路径用反斜杠,或双正斜杠
ffmpegPath := ""
switch runtime.GOOS {
case "linux":
ffmpegPath = filepath.Join(dir, "ffmpeg")
case "windows":
ffmpegPath = filepath.Join(dir, "ffmpeg.exe")
}
// 执行 FFmpeg 命令
cmd := exec.Command(ffmpegPath, cmdArgs...)
// 捕获 FFmpeg 输出(便于调试)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("转码失败: %v, FFmpeg 输出: %s", err, string(output))
}
// 校验输出文件是否生成
if _, err := os.Stat(config.OutputPath); os.IsNotExist(err) {
return fmt.Errorf("转码后输出文件未生成: %s", config.OutputPath)
}
fmt.Printf("转码成功!输入: %s → 输出: %s\n", config.InputPath, config.OutputPath)
return nil
}
func TranscodeToG711AFile(inputFile, outputFile string) error {
// 也可自定义配置(比如调整采样率)
/*
customConfig := TranscodeConfig{
InputPath: "./uploads/test.wav",
OutputPath: "./uploads/test_custom.g711a",
SampleRate: 16000, // 自定义采样率
Channels: 1,
}
err = TranscodeToG711A(customConfig)
*/
// 使用默认配置转码
err := TranscodeToG711A(defaultTranscodeConfig(inputFile, outputFile))
if err != nil {
fmt.Printf("转码失败: %v\n", err)
return fmt.Errorf("转码失败: %v\n", err)
}
return nil
}
// GetMP3Duration 获取MP3文件时长
func GetMP3Duration(filePath string) (time.Duration, error) {
file, err := os.Open(filePath)
if err != nil {
return 0, err
}
defer file.Close()
decoder := mp3.NewDecoder(file)
var frame mp3.Frame
var totalDuration float64
skipped := 0
for {
if err := decoder.Decode(&frame, &skipped); err != nil {
if err == io.EOF {
break
}
return 0, err
}
totalDuration += frame.Duration().Seconds()
}
duration := time.Duration(totalDuration * float64(time.Second))
return duration, nil
}
// GetWAVDurationOptimized 优化的WAV文件时长获取方法
func GetWAVDurationOptimized(filePath string) (time.Duration, error) {
file, err := os.Open(filePath)
if err != nil {
return 0, err
}
defer file.Close()
reader := wav.NewReader(file)
// 使用库提供的Duration方法
duration, err := reader.Duration()
if err != nil {
return 0, err
}
return duration, nil
}
// GetAudioDuration 获取音频文件时长支持MP3和WAV
func GetAudioDuration(filePath string) (time.Duration, string, error) {
// 根据文件扩展名判断文件类型
if len(filePath) > 4 {
ext := filePath[len(filePath)-4:]
switch ext {
case ".mp3":
duration, err := GetMP3Duration(filePath)
return duration, "MP3", err
case ".wav":
duration, err := GetWAVDurationOptimized(filePath)
return duration, "WAV", err
default:
return 0, "", fmt.Errorf("不支持的音频格式: %s", ext)
}
}
// 如果无法从扩展名判断,可以尝试根据文件头部信息判断
return 0, "", fmt.Errorf("无法识别的音频格式")
}

View File

@ -1,2 +1,3 @@
VITE_API_BASE_URL=/api
VITE_WEB_BASE_URL=/
VITE_WEB_BASE_URL=/
VITE_AUDIO_BASE_URL=/

View File

@ -1,2 +1,3 @@
VITE_API_BASE_URL=/extensions/easyaudioencode/api
VITE_WEB_BASE_URL=/extensions/easyaudioencode/web
VITE_WEB_BASE_URL=/extensions/easyaudioencode/web
VITE_AUDIO_BASE_URL=/extensions/easyaudioencode/

View File

@ -10,7 +10,7 @@ export async function GetAudioEncode(data: EncodeReq) {
}
/**
*
*
* @param data
*/
export async function CreateAudioEncode(data: CreateAudioEncodeReq) {
@ -19,39 +19,23 @@ export async function CreateAudioEncode(data: CreateAudioEncodeReq) {
/**
*
* @param id ID
*
* @param id ID
*/
export async function GetAudioEncodeStart(id: number) {
return await GET<AudioEncodeBaseRes>(`/audioencode/${id}/start`);
export async function GetAudioEncodeCode(id: number) {
return await GET<AudioEncodeBaseRes>(`/audioencode/${id}/code`);
}
/**
*
* @param id ID
*/
export async function GetAudioEncodeStop(id: number) {
return await GET<AudioEncodeBaseRes>(`/audioencode/${id}/stop`);
}
/**
*
* @param id ID
*/
export async function GetAudioEncodeSnap(id: number) {
return await GET<AudioEncodeBaseRes>(`/audioencode/${id}/snap`);
}
/**
*
* @param id ID
*
* @param id ID
*/
export async function GetAudioEncodeById(id: string) {
return await GET<AudioEncodeDetailRes>(`/audioencode/${id}`);
}
/**
*
*
* @param data id
*/
export async function UpdateAudioEncode(data: UpdateAudioEncodeReq) {
@ -60,8 +44,8 @@ export async function UpdateAudioEncode(data: UpdateAudioEncodeReq) {
}
/**
*
* @param id ID
*
* @param id ID
*/
export async function DeleteAudioEncode(id: number) {
return await DELETE<AudioEncodeBaseRes>(`/audioencode/${id}`);

View File

@ -7,7 +7,7 @@ const codeMessage: { [key: number]: string } = {
201: "新建或修改数据成功。",
202: "一个请求已经进入后台排队(异步任务)。",
204: "删除数据成功。",
400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
// 400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
401: "用户没有权限(令牌、用户名、密码错误)。",
403: "用户得到授权,但是访问是被禁止的。",
404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",

44
web/src/api/task.ts Normal file
View File

@ -0,0 +1,44 @@
import { GET, POST, PUT, DELETE } from "./http";
import type { AudioTaskBaseRes, AudioTaskRes, CreateAudioTaskReq, TaskReq, DelAudioTaskReq } from "../types/audiotask";
/**
*
* @returns
*/
export async function GetAudioTask(data: TaskReq) {
return await GET<AudioTaskRes>(`/audiotask`, data);
}
/**
*
* @param data
*/
export async function CreateAudioTask(data: CreateAudioTaskReq) {
return await POST<AudioTaskBaseRes>(`/audiotask`, data);
}
/**
*
* @param id ID
*/
export async function GetAudioTaskCancel(id: number) {
return await GET<AudioTaskBaseRes>(`/audiotask/cancel/${id}`);
}
/**
*
* @param id ID
*/
export async function DeleteAudioTask(id: number) {
return await DELETE<AudioTaskBaseRes>(`/audiotask/${id}`);
}
/**
*
* @param ids ID
*/
export async function DeleteAudioTaskAll(data: DelAudioTaskReq) {
return await DELETE<AudioTaskBaseRes>(`/audiotask`, data);
}

View File

@ -1,9 +1,11 @@
import { useRef, useState, useMemo } from "react";
import { Table, Button, Space, Popconfirm, Flex, message, Tag, Tooltip } from "antd";
import { PlayCircleOutlined, EditOutlined, NotificationOutlined, DeleteOutlined } from "@ant-design/icons";
import { Table, Button, Space, Popconfirm, Flex, message, Tag, Tooltip, notification } from "antd";
import { PlayCircleOutlined, EditOutlined, NotificationOutlined, DeleteOutlined, ReloadOutlined } from "@ant-design/icons";
import { useQuery, useMutation } from "@tanstack/react-query";
import { GetAudioEncode, DeleteAudioEncode, GetAudioEncodeStart, GetAudioEncodeStop } from "../api/audio";
import { GetAudioEncode, DeleteAudioEncode, GetAudioEncodeCode } from "../api/audio";
import { CreateAudioTask } from "../api/task";
import type { AudioEncodeItem } from "../types/audioencode";
import type { AddTaskItem } from "../types/audiotask";
import type { ColumnsType } from "antd/es/table";
import AddAudioEncode, { AddAudioEncodeRef } from "./AddAudioEncode";
import PlayerModal, { PlayerModalRef } from "./PlayerModal";
@ -11,8 +13,8 @@ import ChannelModel, { IChannelModelFunc } from "./channel/Channel";
import UploadAudio from './UploadAudio';
import { useGlobal } from "../Context";
import { FormatFileSizeToString } from "../utils/rate";
import { formatSecondsToHMS } from "../utils/time";
import Filter from "./Filter";
export default function AudioEncodePage() {
const { ErrorHandle } = useGlobal();
const dialogRef = useRef<AddAudioEncodeRef>(null);
@ -59,32 +61,42 @@ export default function AudioEncodePage() {
ErrorHandle(error);
},
});
const { mutate: startMutation } = useMutation({
mutationFn: GetAudioEncodeStart,
// 转码任务
const [codeLoadings, setCodeLoadings] = useState<number[]>([]);
const { mutate: codeMutation } = useMutation({
mutationFn: GetAudioEncodeCode,
onMutate: (id: number) => {
setDelLoadings((prev) => [...prev, id]);
},
onSuccess: (_, ctx) => {
message.success("开启成功");
setDelLoadings((prev) => prev.filter((item) => item !== ctx));
message.success("转码成功");
refetch();
},
onError: (error: Error, ctx) => {
setDelLoadings((prev) => prev.filter((item) => item !== ctx));
ErrorHandle(error);
},
});
const { mutate: stopMutation } = useMutation({
mutationFn: GetAudioEncodeStop,
onMutate: (id: number) => {
},
onSuccess: (_, ctx) => {
message.success("关闭成功");
refetch();
},
onError: (error: Error, ctx) => {
ErrorHandle(error);
const { mutate: createTaskMutate, isPending: creating } = useMutation({
mutationFn: CreateAudioTask,
onSuccess: (v, ctx) => {
if (v.data.data == 'ok') {
message.success("下发任务成功");
} else {
notification.error({
message: '下发任务失败,可以重新下发!',
description:
JSON.stringify(v.data.data),
});
}
},
onError: ErrorHandle,
retry: 0,
});
// 打开新增模态框
const handleAdd = () => {
dialogRef.current?.open();
@ -95,7 +107,7 @@ export default function AudioEncodePage() {
dialogRef.current?.open(disk);
};
const handleNotification = (disk: AudioEncodeItem) => {
channelRef.current?.openModal(disk.id)
channelRef.current?.openModal(disk.id, disk.name)
}
const handlePlay = (disk: AudioEncodeItem) => {
playerModalRef.current?.open(disk);
@ -134,7 +146,16 @@ export default function AudioEncodePage() {
dataIndex: "name",
align: "center",
},
{
title: "文件名称",
dataIndex: "file_name",
align: "center",
render: (text, record) => (
<Space>
{text}
</Space>
),
},
{
title: "类型",
dataIndex: "mode",
@ -156,16 +177,18 @@ export default function AudioEncodePage() {
),
},
{
title: "文件名称",
dataIndex: "file_name",
title: "时长",
dataIndex: "duration",
align: "center",
render: (text, record) => (
<Space>
{text}
{formatSecondsToHMS(text)}
</Space>
),
},
{
title: "状态",
dataIndex: "encode_status",
@ -175,7 +198,16 @@ export default function AudioEncodePage() {
{text == 0 && <Tag color="#999"></Tag>}
{text == 1 && <Tag color="#87d068"></Tag>}
{text == 2 && <Tag color="#87d068"></Tag>}
{text == 3 && <Tag color="#f50"></Tag>}
{text == 3 && <>
<Tag color="#f50"></Tag>
<Tooltip placement="top" title="重新转码" color="#fff">
<Button
onClick={() => codeMutation(record.id)}
loading={codeLoadings.includes(record.id)}
icon={<ReloadOutlined />}
/>
</Tooltip>
</>}
</Space>
),
},
@ -297,8 +329,8 @@ export default function AudioEncodePage() {
onSuccess={() => refetch()}
/>
{/* 广播模态框 */}
<ChannelModel ref={channelRef} onCallback={(id: any, bid: any) => {
console.log(id, bid);
<ChannelModel ref={channelRef} onCallback={(item: AddTaskItem[]) => {
createTaskMutate({ items: item })
}} />
</div>
);

View File

@ -0,0 +1,303 @@
import { useRef, useState, useMemo } from "react";
import { Table, Button, Space, Popconfirm, Flex, message, Tag, Tooltip } from "antd";
import { CloseOutlined, DeleteOutlined, ReloadOutlined } from "@ant-design/icons";
import { useQuery, useMutation } from "@tanstack/react-query";
import { GetAudioTask, DeleteAudioTask, GetAudioTaskCancel, DeleteAudioTaskAll } from "../api/task";
import type { AudioTaskItem } from "../types/audiotask";
import type { ColumnsType } from "antd/es/table";
import UploadAudio from './UploadAudio';
import { useGlobal } from "../Context";
import { FormatFileSizeToString } from "../utils/rate";
import { formatSecondsToHMS } from "../utils/time";
import Filter from "./Filter";
export default function AudioTaskPage() {
const { ErrorHandle } = useGlobal();
const [pagination, setPagination] = useState({
page: 1,
size: 10,
name: ""
});
// 获取任务列表
const {
data: storageResponse,
isLoading,
refetch,
} = useQuery({
queryKey: ["storage", pagination],
queryFn: () =>
GetAudioTask({ ...pagination })
.then((res) => res.data)
.catch((err) => {
ErrorHandle(err);
throw err;
}),
refetchInterval: 4000,
retry: 2,
});
// 删除任务
const [delLoadings, setDelLoadings] = useState<number[]>([]);
const { mutate: deleteMutation } = useMutation({
mutationFn: DeleteAudioTask,
onMutate: (id: number) => {
setDelLoadings((prev) => [...prev, id]);
},
onSuccess: (_, ctx) => {
setDelLoadings((prev) => prev.filter((item) => item !== ctx));
message.success("删除成功");
refetch();
},
onError: (error: Error, ctx) => {
setDelLoadings((prev) => prev.filter((item) => item !== ctx));
ErrorHandle(error);
},
});
// 批量删除任务
const { mutate: deleteMutationAll, isPending: delAllLoadings } = useMutation({
mutationFn: DeleteAudioTaskAll,
onSuccess: () => {
message.success("批量删除成功");
refetch()
setSelectedRowKeys([])
},
onError: ErrorHandle,
retry: 0,
});
// 取消任务
const [cancelLoadings, setCancelLoadings] = useState<number[]>([]);
const { mutate: cancelMutation } = useMutation({
mutationFn: GetAudioTaskCancel,
onMutate: (id: number) => {
// setDelLoadings((prev) => [...prev, id]);
},
onSuccess: (_, ctx) => {
message.success("取消成功");
refetch();
},
onError: (error: Error, ctx) => {
ErrorHandle(error);
},
});
// 处理分页变化
const handleTableChange = (page: number, pageSize?: number) => {
setPagination((prev) => ({
...prev,
page: page,
size: pageSize || prev.size,
}));
};
// 客户端分页数据
const dataSource = useMemo(() => {
const items = storageResponse?.items || [];
const start = (pagination.page - 1) * pagination.size;
const end = start + pagination.size;
return items.slice(start, end);
}, [storageResponse, pagination]);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const rowSelection = {
selectedRowKeys,
onChange: (
newSelectedRowKeys: React.Key[],
selectedRows: AudioTaskItem[]
) => {
setSelectedRowKeys([...newSelectedRowKeys]);
},
};
// 表格列定义
const columns: ColumnsType<AudioTaskItem> = [
{
title: "任务编码",
dataIndex: "id",
align: "center",
},
{
title: "音频名称",
dataIndex: "audio_name",
align: "center",
},
{
title: "通道名称",
dataIndex: "channel_name",
align: "center",
},
{
title: "通道编码",
dataIndex: "channel_id",
align: "center",
},
{
title: "类型",
dataIndex: "mode",
align: "center",
render: (text, record) => (
<Space>
{text}
</Space>
),
},
{
title: "大小",
dataIndex: "size",
align: "center",
render: (text, record) => (
<Space>
{FormatFileSizeToString(text)}
</Space>
),
},
{
title: "时长",
dataIndex: "duration",
align: "center",
render: (text, record) => (
<Space>
{formatSecondsToHMS(text)}
</Space>
),
},
{
title: "状态",
dataIndex: "task_status",
align: "center",
render: (text, record) => (
<Space>
{text == 0 && <Tag className="mr-0" color="gold"></Tag>}
{text == 1 && <Tag className="mr-0" color="green"></Tag>}
{text == 2 && <Tag className="mr-0" color="#87d068"></Tag>}
{text == 3 && <Tag className="mr-0" color="#999"></Tag>}
{text == 4 && <>
<Tooltip placement="top" title={record.error_msg} color="#fff">
<Tag className="mr-0" color="red"></Tag>
</Tooltip>
</>}
{text < 2 && <>
<Tooltip placement="top" title="取消任务" color="#fff">
<Button
shape="circle"
size="small"
onClick={() => cancelMutation(record.id)}
loading={cancelLoadings.includes(record.id)}
icon={<CloseOutlined />}
/>
</Tooltip>
</>}
</Space>
),
},
{
title: "创建日期",
dataIndex: "created_at",
align: "center",
render: (text: string) => (text ? new Date(text).toLocaleString() : "-"),
},
// {
// title: "开始日期",
// dataIndex: "start_time",
// align: "center",
// render: (text: string) => (text ? new Date(text).toLocaleString() : "-"),
// },
// {
// title: "结束日期",
// dataIndex: "end_time",
// align: "center",
// render: (text: string) => (text ? new Date(text).toLocaleString() : "-"),
// },
// {
// title: "日期",
// align: "center",
// render: (_, record) => (
// <>
// <div><span>创建: </span>{record.created_at ? new Date(record.created_at).toLocaleString() : "-"}</div>
// <div><span>更新: </span>{record.updated_at ? new Date(record.updated_at).toLocaleString() : "-"}</div>
// </>
// ),
// },
{
title: "操作",
align: "center",
width: 120,
fixed: "right",
render: (_, record) => (
<Space>
<Popconfirm
title="确定要删除这个文件吗?"
onConfirm={() => {
if (record.id) {
deleteMutation(record.id);
}
}}
okText="确定"
cancelText="取消"
>
<Button
loading={delLoadings.includes(record.id)}
danger
icon={<DeleteOutlined />}
/>
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Flex justify="space-between" align="center" className="mb-4">
<Space>
<Popconfirm
title="确定要批量删除文件吗?"
onConfirm={() => {
deleteMutationAll({ids:selectedRowKeys as number[]})
}}
okText="确定"
cancelText="取消"
>
<Button color="danger" variant="solid" loading={delAllLoadings} disabled={selectedRowKeys.length == 0} >
</Button>
</Popconfirm>
</Space>
<Filter
searchLoading={isLoading}
onSearchChange={(value: string) => {
setPagination({ ...pagination, name: value });
}}
/>
</Flex>
{/* 表格 */}
<Table
columns={columns}
rowSelection={rowSelection}
dataSource={storageResponse?.items}
rowKey="id"
loading={isLoading}
scroll={{ x: "max-content" }}
pagination={{
current: pagination.page,
pageSize: pagination.size,
total: storageResponse?.total || 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
onChange: handleTableChange,
onShowSizeChange: handleTableChange,
}}
/>
</div>
);
}

View File

@ -67,7 +67,7 @@ const Filter: React.FC<IFilterProps> = ({
<AutoSearch
onSearch={onSearchChange}
loading={searchLoading}
placeholder={"请输入文件名称"}
placeholder={"请输入关键字"}
/>
</div>
</Space>

View File

@ -5,7 +5,7 @@ import { CreateAudioEncode, UpdateAudioEncode } from "../api/audio";
import type { CreateAudioEncodeReq, AudioEncodeItem } from "../types/audioencode";
import { useGlobal } from "../Context";
import ReactAudioPlayer from 'react-audio-player';
const apiAudioUrl = import.meta.env.VITE_AUDIO_BASE_URL;
interface PlayerModalProps {
title: string;
onSuccess: () => void;
@ -24,7 +24,7 @@ const PlayerModal = forwardRef<PlayerModalRef, PlayerModalProps>(
useImperativeHandle(ref, () => ({
open: (task?: AudioEncodeItem) => {
if (task?.source_url) {
setAudioUrl(task?.source_url)
setAudioUrl(`${location.origin}${apiAudioUrl}${task?.source_url}`)
}
if (task?.name) {
setAudioName(task?.name)

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Upload, Button, message, UploadFile, UploadProps } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
// 音频文件上传组件
const AudioUploader: React.FC<{
onOk: ()=> void;
@ -24,7 +24,7 @@ import { UploadOutlined } from '@ant-design/icons';
formData.append('audio', file); // 与后端接收的参数名保持一致
// 发送请求到 Go 后端
const response = await fetch('/api/audioencode/upload', {
const response = await fetch(apiBaseUrl+'/audioencode/upload', {
method: 'POST',
body: formData,
headers: {

View File

@ -1,4 +1,5 @@
import { Alert, ConfigProvider, Modal, Tag, message } from "antd";
import { Space, ConfigProvider, Modal, Tag, Tooltip, Button } from "antd";
import { DeliveredProcedureOutlined } from "@ant-design/icons";
import Table, { ColumnsType } from "antd/es/table";
import React, {
forwardRef,
@ -12,28 +13,30 @@ import { ChannelItem, ChannelReq } from "../../types/device";
import Filter from "./Filter";
import { GetChannels } from "../../api/devices";
import { useGlobal } from "../../Context";
import type { AddTaskItem } from "../../types/audiotask";
export interface IChannelModelFunc {
openModal: (id: number) => void;
openModal: (id: number, name: string) => void;
}
interface IChannelModel {
ref: any;
onCallback: (id: any, bid:any) => void;
onCallback: (data: AddTaskItem[]) => void;
}
const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
const ChannelModel: React.FC<IChannelModel> = forwardRef(({ onCallback }, ref) => {
useImperativeHandle(ref, () => ({
openModal: (id: string) => {
openModal: (id: number, name: string) => {
setOpen(true);
if (id != "") {
setSelectedRowKeys([id])
}
// if (id != 0) {
// setSelectedRowKeys([id])
// }
audioName.current = name
pid.current = id;
},
}));
const [open, setOpen] = useState(false);
const pid = useRef<string>(undefined);
const pid = useRef<number>(0);
const audioName = useRef<string>('');
const { ErrorHandle } = useGlobal();
const columns: ColumnsType<ChannelItem> = [
@ -74,6 +77,25 @@ const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
dataIndex: "protocol",
render: (text: string) => text || "-",
},
{
title: "操作",
align: "center",
width: 120,
fixed: "right",
render: (_, record) => (
<Space>
<Tooltip placement="top" title="下发广播任务" color="#fff">
<Button icon={<DeliveredProcedureOutlined />} onClick={() => {
onCallback([{
audio_id: pid.current,
channel_id: record.id,
channel_name: record.name,
}])
}} />
</Tooltip>
</Space>
),
},
];
// 获取通道列表
@ -82,10 +104,10 @@ const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
size: 10, // 通道一般 < 10 个,客户端不做分页,一次性全查
device_id: "",
pid: "ROOT",
status: "",
status: true,
name: "",
bid: "",
protocol:"GB28181"
protocol: "GB28181"
});
const { data, isLoading } = useQuery({
@ -100,6 +122,7 @@ const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
enabled: open,
});
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [selectedRowsAll, setSelectedRows] = useState<ChannelItem[]>([]);
const rowSelection = {
selectedRowKeys,
getCheckboxProps: (record: ChannelItem) => ({
@ -109,17 +132,34 @@ const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
newSelectedRowKeys: React.Key[],
selectedRows: ChannelItem[]
) => {
if (newSelectedRowKeys.length > 0) {
setSelectedRowKeys([newSelectedRowKeys[newSelectedRowKeys.length - 1]]);
}
setSelectedRowKeys(newSelectedRowKeys);
setSelectedRows(selectedRows)
// if (newSelectedRowKeys.length > 0) {
// setSelectedRowKeys([newSelectedRowKeys[newSelectedRowKeys.length - 1]]);
// }
},
};
const onAll = () => {
let dataItem: AddTaskItem[] = []
selectedRowsAll.forEach(record => {
let list: AddTaskItem = {
audio_id: pid.current,
channel_id: record.id,
channel_name: record.name,
}
dataItem.push(list)
});
onCallback(dataItem)
setOpen(false);
setSelectedRows([])
setSelectedRowKeys([])
}
const onCancel = () => {
setOpen(false);
if (selectedRowKeys.length>0) {
onCallback(selectedRowKeys[0], pid.current)
}
// if (selectedRowKeys.length>0) {
// onCallback(selectedRowKeys[0], pid.current)
// }
setSelectedRows([])
setSelectedRowKeys([])
};
@ -144,11 +184,17 @@ const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
>
<Modal
open={open}
title="选中通道"
title={`选择通道广播【${audioName.current}】音频`}
style={{ top: "5%" }}
width={"1000px"}
onCancel={onCancel}
onOk={onCancel}
footer={
<>
<Button onClick={() => onCancel()} > </Button>
<Button disabled={selectedRowKeys.length == 0} type="primary" onClick={() => onAll()} className="mr-6"></Button>
</>
}
onCancel={onCancel}
// onOk={onCancel}
>
<div>
<div className="mb-2 flex justify-end">
@ -158,7 +204,7 @@ const ChannelModel: React.FC<IChannelModel> = forwardRef(({onCallback},ref) => {
onSearchChange={(value: string) => {
setPagination({ ...pagination, name: value, bid: value });
}}
onSelectChange={(value: string) => {
onSelectChange={(value: any) => {
setPagination({ ...pagination, status: value });
}}
/>

View File

@ -6,6 +6,7 @@ import {
import Box from "../components/Box";
import { useState } from "react";
import AudioEncodePage from "../components/AudioEncode";
import AudioTaskPage from "../components/AudioTask";
type MenuItem = Required<MenuProps>["items"][number];
export default function Home() {
@ -14,12 +15,12 @@ export default function Home() {
const items: MenuItem[] = [
{
key: "sub0",
label: "推流任务",
label: "文件管理",
icon: <FileSearchOutlined />,
},
{
key: "sub1",
label: "基础配置",
label: "任务列表",
icon: <UnorderedListOutlined />,
},
];
@ -30,7 +31,7 @@ export default function Home() {
return (
<Row gutter={16}>
{/* <div>
<div>
<Affix className="hidden lg:block" offsetTop={0}>
<Box
style={{ minWidth: "10rem", width: "12rem" }}
@ -57,9 +58,9 @@ export default function Home() {
items={items}
/>
</Box>
</div> */}
{/* <Col sm={24} md={24} lg={18} xl={18} xxl={18} className="w-full"> */}
<Col sm={24} md={24} className="w-full">
</div>
<Col sm={24} md={24} lg={18} xl={18} xxl={18} className="w-full">
{/* <Col sm={24} md={24} className="w-full"> */}
{currentMenu == "sub0" && (
<Box>
<AudioEncodePage />
@ -67,7 +68,7 @@ export default function Home() {
)}
{currentMenu == "sub1" && (
<Box>
<AudioTaskPage />
</Box>
)}
</Col>

View File

@ -0,0 +1,83 @@
/**
*
*/
export type AudioTaskBaseRes = {
data: string;
};
/**
*
*/
export type AudioTaskRes = {
items: AudioTaskItem[];
/**
*
*/
total: number;
};
/**
*
*/
export type AudioTaskItem = {
id: number;
audio_id: number;
audio_name: string;
channel_id: string;
channel_name: string;
size: number;
duration: number;
mode: string;
created_at: string;
updated_at: string;
task_status: number;
end_time: string;
create_time: string;
start_time: string;
error_msg: string;
};
/**
*
*/
export type TaskReq = {
/**
*
*/
audio_name?: string;
/**
* (1~N)
*/
page: number;
/**
* (10~100)
*/
size: number;
}
export type AddTaskItem = {
/**
*
*/
audio_id: number;
/**
*
*/
channel_name: string;
/**
* ID
*/
channel_id: string;
};
export type CreateAudioTaskReq = {
items: AddTaskItem[];
};
export type DelAudioTaskReq = {
/**
* IDs
*/
ids: number[];
};

View File

@ -189,7 +189,7 @@ type ChannelReq = {
/**
* true:线; false:线;
*/
status?: string;
status?: boolean;
};
export type ChannelRes = {

19
web/src/utils/time.ts Normal file
View File

@ -0,0 +1,19 @@
/**
* "HH:mm:ss"
* @param {number} totalSeconds -
* @returns {string} -
*/
export function formatSecondsToHMS(totalSeconds: number) :string{
if (totalSeconds <= 0) return "";
// 计算小时、分钟和秒
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60);
const seconds = totalSeconds - (hours * 3600) - (minutes * 60);
// 使用 padStart(2, '0') 来确保每个部分都是两位数,不足的用 '0' 填充
const formattedHours = hours.toString().padStart(2, '0');
const formattedMinutes = minutes.toString().padStart(2, '0');
const formattedSeconds = seconds.toString().padStart(2, '0');
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
}