diff --git a/.gitignore b/.gitignore index fdaaa8c..3a89eb0 100644 --- a/.gitignore +++ b/.gitignore @@ -67,5 +67,5 @@ dist-ssr *.njsproj *.sln *.sw? - +uploads/* dish* \ No newline at end of file diff --git a/deploy/easyaudioencode/1.png b/deploy/easyaudioencode/1.png index b2f7bb7..14ab8dd 100644 Binary files a/deploy/easyaudioencode/1.png and b/deploy/easyaudioencode/1.png differ diff --git a/deploy/easyaudioencode/EasyAudioEncode.html b/deploy/easyaudioencode/EasyAudioEncode.html index fbf4fa9..60bbc5c 100644 --- a/deploy/easyaudioencode/EasyAudioEncode.html +++ b/deploy/easyaudioencode/EasyAudioEncode.html @@ -17,7 +17,7 @@ } .img-box { - max-width: 260px; + max-width: 660px; margin: auto; } diff --git a/deploy/easyaudioencode/EasyAudioEncode.ico b/deploy/easyaudioencode/EasyAudioEncode.ico index ee88bf2..85a1228 100644 Binary files a/deploy/easyaudioencode/EasyAudioEncode.ico and b/deploy/easyaudioencode/EasyAudioEncode.ico differ diff --git a/go.mod b/go.mod index 6cffd32..c349e76 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 091a9a7..4b7af0b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/app/app.go b/internal/app/app.go index 633b48f..454a9b5 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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 diff --git a/internal/core/audioencode/audioencode.go b/internal/core/audioencode/audioencode.go index 74e9005..b44309a 100644 --- a/internal/core/audioencode/audioencode.go +++ b/internal/core/audioencode/audioencode.go @@ -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 diff --git a/internal/core/audioencode/audioencode.param.go b/internal/core/audioencode/audioencode.param.go index 33103bd..c12fc67 100644 --- a/internal/core/audioencode/audioencode.param.go +++ b/internal/core/audioencode/audioencode.param.go @@ -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"` // 时长 } diff --git a/internal/core/audioencode/audiotask.go b/internal/core/audioencode/audiotask.go new file mode 100644 index 0000000..f700199 --- /dev/null +++ b/internal/core/audioencode/audiotask.go @@ -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 +} diff --git a/internal/core/audioencode/audiotask.param.go b/internal/core/audioencode/audiotask.param.go new file mode 100644 index 0000000..01df852 --- /dev/null +++ b/internal/core/audioencode/audiotask.param.go @@ -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 +} diff --git a/internal/core/audioencode/core.go b/internal/core/audioencode/core.go index 54be834..5ae4dec 100644 --- a/internal/core/audioencode/core.go +++ b/internal/core/audioencode/core.go @@ -4,6 +4,7 @@ package audioencode // Storer data type Storer interface { AudioEncode() AudioEncodeStorer + AudioTask() AudioTaskStorer } // Core diff --git a/internal/core/audioencode/model.go b/internal/core/audioencode/model.go index 0440c7b..959bab5 100644 --- a/internal/core/audioencode/model.go +++ b/internal/core/audioencode/model.go @@ -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" +} diff --git a/internal/core/audioencode/store/audioencodedb/audioencode.go b/internal/core/audioencode/store/audioencodedb/audioencode.go index e3326e2..8d5b237 100644 --- a/internal/core/audioencode/store/audioencodedb/audioencode.go +++ b/internal/core/audioencode/store/audioencodedb/audioencode.go @@ -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 diff --git a/internal/core/audioencode/store/audioencodedb/audiotask.go b/internal/core/audioencode/store/audioencodedb/audiotask.go new file mode 100644 index 0000000..91eefbb --- /dev/null +++ b/internal/core/audioencode/store/audioencodedb/audiotask.go @@ -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 +} diff --git a/internal/core/audioencode/store/audioencodedb/db.go b/internal/core/audioencode/store/audioencodedb/db.go index a340520..63cde91 100644 --- a/internal/core/audioencode/store/audioencodedb/db.go +++ b/internal/core/audioencode/store/audioencodedb/db.go @@ -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) } diff --git a/internal/core/host/core.go b/internal/core/host/core.go index ac67a2c..20f2bfa 100644 --- a/internal/core/host/core.go +++ b/internal/core/host/core.go @@ -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 +} diff --git a/internal/core/host/talk.go b/internal/core/host/talk.go new file mode 100644 index 0000000..d315a26 --- /dev/null +++ b/internal/core/host/talk.go @@ -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 +} diff --git a/internal/core/host/talk.param.go b/internal/core/host/talk.param.go new file mode 100644 index 0000000..421d053 --- /dev/null +++ b/internal/core/host/talk.param.go @@ -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"` +} diff --git a/internal/core/transcode/code.go b/internal/core/transcode/code.go new file mode 100644 index 0000000..a029e39 --- /dev/null +++ b/internal/core/transcode/code.go @@ -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() +} diff --git a/internal/core/transcode/core.go b/internal/core/transcode/core.go index 3c49827..4155798 100644 --- a/internal/core/transcode/core.go +++ b/internal/core/transcode/core.go @@ -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 +} diff --git a/internal/core/transcode/task.go b/internal/core/transcode/task.go new file mode 100644 index 0000000..9bd7fb5 --- /dev/null +++ b/internal/core/transcode/task.go @@ -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) +} diff --git a/internal/web/api/api.go b/internal/web/api/api.go index 81904ab..a7f46f5 100644 --- a/internal/web/api/api.go +++ b/internal/web/api/api.go @@ -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") diff --git a/internal/web/api/audioencode.go b/internal/web/api/audioencode.go index 139382f..2f1a68e 100644 --- a/internal/web/api/audioencode.go +++ b/internal/web/api/audioencode.go @@ -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 } diff --git a/internal/web/api/audiotask.go b/internal/web/api/audiotask.go new file mode 100644 index 0000000..d7aa552 --- /dev/null +++ b/internal/web/api/audiotask.go @@ -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 +} diff --git a/internal/web/api/update.go b/internal/web/api/update.go index 102e0ea..5abd5d7 100644 --- a/internal/web/api/update.go +++ b/internal/web/api/update.go @@ -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 } diff --git a/pkg/ffmpeg/core.go b/pkg/ffmpeg/core.go new file mode 100644 index 0000000..8edcc5d --- /dev/null +++ b/pkg/ffmpeg/core.go @@ -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 Hz,G711A 标准采样率) + 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 转 G711A(PCMA) +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", // 指定输出格式为 G711A(PCMA) + "-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("无法识别的音频格式") +} diff --git a/uploads/source/0148129c-0e87-46a1-9c21-de56f86f7be4.mp3 b/uploads/source/0148129c-0e87-46a1-9c21-de56f86f7be4.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/0148129c-0e87-46a1-9c21-de56f86f7be4.mp3 and /dev/null differ diff --git a/uploads/source/13b2178f-49c8-46df-a410-48cae25ffce3.mp3 b/uploads/source/13b2178f-49c8-46df-a410-48cae25ffce3.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/13b2178f-49c8-46df-a410-48cae25ffce3.mp3 and /dev/null differ diff --git a/uploads/source/1766641757629720400_3773591637.mp3 b/uploads/source/1766641757629720400_3773591637.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/1766641757629720400_3773591637.mp3 and /dev/null differ diff --git a/uploads/source/1766641905953962100_3773591637.mp3 b/uploads/source/1766641905953962100_3773591637.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/1766641905953962100_3773591637.mp3 and /dev/null differ diff --git a/uploads/source/1766642108747462200_3773591637.mp3 b/uploads/source/1766642108747462200_3773591637.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/1766642108747462200_3773591637.mp3 and /dev/null differ diff --git a/uploads/source/1766642355768565400_3773591637.mp3 b/uploads/source/1766642355768565400_3773591637.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/1766642355768565400_3773591637.mp3 and /dev/null differ diff --git a/uploads/source/2f5ca40a-5dbe-4198-9457-99264e18f910.mp3 b/uploads/source/2f5ca40a-5dbe-4198-9457-99264e18f910.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/2f5ca40a-5dbe-4198-9457-99264e18f910.mp3 and /dev/null differ diff --git a/uploads/source/3d33237c-ab7c-463f-a51c-a0034b842e4b.mp3 b/uploads/source/3d33237c-ab7c-463f-a51c-a0034b842e4b.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/3d33237c-ab7c-463f-a51c-a0034b842e4b.mp3 and /dev/null differ diff --git a/uploads/source/4981c2a9-1462-4640-af9a-3a285aa802ca.mp3 b/uploads/source/4981c2a9-1462-4640-af9a-3a285aa802ca.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/4981c2a9-1462-4640-af9a-3a285aa802ca.mp3 and /dev/null differ diff --git a/uploads/source/64e77831-7369-4dc4-8c6b-1ecda5b1c084.mp3 b/uploads/source/64e77831-7369-4dc4-8c6b-1ecda5b1c084.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/64e77831-7369-4dc4-8c6b-1ecda5b1c084.mp3 and /dev/null differ diff --git a/uploads/source/7c97fb4c-76b7-45c8-95ac-95964ae1cdaf.mp3 b/uploads/source/7c97fb4c-76b7-45c8-95ac-95964ae1cdaf.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/7c97fb4c-76b7-45c8-95ac-95964ae1cdaf.mp3 and /dev/null differ diff --git a/uploads/source/a4776c9d-0f09-47c8-bae8-c10137cfbeae.mp3 b/uploads/source/a4776c9d-0f09-47c8-bae8-c10137cfbeae.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/a4776c9d-0f09-47c8-bae8-c10137cfbeae.mp3 and /dev/null differ diff --git a/uploads/source/b18c59d9-cf4a-4d78-a785-e8cdbae63cf4.mp3 b/uploads/source/b18c59d9-cf4a-4d78-a785-e8cdbae63cf4.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/b18c59d9-cf4a-4d78-a785-e8cdbae63cf4.mp3 and /dev/null differ diff --git a/uploads/source/d1b3ddbb-74da-405d-a23c-65942766b76b.mp3 b/uploads/source/d1b3ddbb-74da-405d-a23c-65942766b76b.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/d1b3ddbb-74da-405d-a23c-65942766b76b.mp3 and /dev/null differ diff --git a/uploads/source/dad2f7cb-5250-4a51-8ad8-3a5f34ed3b97.mp3 b/uploads/source/dad2f7cb-5250-4a51-8ad8-3a5f34ed3b97.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/dad2f7cb-5250-4a51-8ad8-3a5f34ed3b97.mp3 and /dev/null differ diff --git a/uploads/source/db5649dd-b10f-4ffb-83f1-cd792a59e011.mp3 b/uploads/source/db5649dd-b10f-4ffb-83f1-cd792a59e011.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/db5649dd-b10f-4ffb-83f1-cd792a59e011.mp3 and /dev/null differ diff --git a/uploads/source/f52e76f1-2f64-47d2-9df8-982f506b255e.mp3 b/uploads/source/f52e76f1-2f64-47d2-9df8-982f506b255e.mp3 deleted file mode 100644 index a1dc171..0000000 Binary files a/uploads/source/f52e76f1-2f64-47d2-9df8-982f506b255e.mp3 and /dev/null differ diff --git a/web/.env.development b/web/.env.development index 35d5d5a..7e8c06b 100644 --- a/web/.env.development +++ b/web/.env.development @@ -1,2 +1,3 @@ VITE_API_BASE_URL=/api -VITE_WEB_BASE_URL=/ \ No newline at end of file +VITE_WEB_BASE_URL=/ +VITE_AUDIO_BASE_URL=/ \ No newline at end of file diff --git a/web/.env.production b/web/.env.production index dcaec2d..a821019 100644 --- a/web/.env.production +++ b/web/.env.production @@ -1,2 +1,3 @@ VITE_API_BASE_URL=/extensions/easyaudioencode/api -VITE_WEB_BASE_URL=/extensions/easyaudioencode/web \ No newline at end of file +VITE_WEB_BASE_URL=/extensions/easyaudioencode/web +VITE_AUDIO_BASE_URL=/extensions/easyaudioencode/ \ No newline at end of file diff --git a/web/src/api/audio.ts b/web/src/api/audio.ts index 4a61e06..5a3633c 100644 --- a/web/src/api/audio.ts +++ b/web/src/api/audio.ts @@ -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(`/audioencode/${id}/start`); +export async function GetAudioEncodeCode(id: number) { + return await GET(`/audioencode/${id}/code`); } /** - * 关闭推流 - * @param id 任务ID - */ -export async function GetAudioEncodeStop(id: number) { - return await GET(`/audioencode/${id}/stop`); -} - -/** - * 获取快照 - * @param id 任务ID - */ -export async function GetAudioEncodeSnap(id: number) { - return await GET(`/audioencode/${id}/snap`); -} - -/** - * 获取任务详情 - * @param id 任务ID + * 获取详情 + * @param id ID */ export async function GetAudioEncodeById(id: string) { return await GET(`/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(`/audioencode/${id}`); diff --git a/web/src/api/http.ts b/web/src/api/http.ts index 0da0ced..b898b06 100644 --- a/web/src/api/http.ts +++ b/web/src/api/http.ts @@ -7,7 +7,7 @@ const codeMessage: { [key: number]: string } = { 201: "新建或修改数据成功。", 202: "一个请求已经进入后台排队(异步任务)。", 204: "删除数据成功。", - 400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。", + // 400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。", 401: "用户没有权限(令牌、用户名、密码错误)。", 403: "用户得到授权,但是访问是被禁止的。", 404: "发出的请求针对的是不存在的记录,服务器没有进行操作。", diff --git a/web/src/api/task.ts b/web/src/api/task.ts new file mode 100644 index 0000000..a2d8254 --- /dev/null +++ b/web/src/api/task.ts @@ -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(`/audiotask`, data); +} + +/** + * 创建 + * @param data 创建参数 + */ +export async function CreateAudioTask(data: CreateAudioTaskReq) { + return await POST(`/audiotask`, data); +} + + +/** + * 取消 + * @param id ID + */ +export async function GetAudioTaskCancel(id: number) { + return await GET(`/audiotask/cancel/${id}`); +} + + +/** + * 删除 + * @param id ID + */ +export async function DeleteAudioTask(id: number) { + return await DELETE(`/audiotask/${id}`); +} + +/** + * 批量删除 + * @param ids ID + */ +export async function DeleteAudioTaskAll(data: DelAudioTaskReq) { + return await DELETE(`/audiotask`, data); +} diff --git a/web/src/components/AudioEncode.tsx b/web/src/components/AudioEncode.tsx index bb308d1..22b93d4 100644 --- a/web/src/components/AudioEncode.tsx +++ b/web/src/components/AudioEncode.tsx @@ -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(null); @@ -59,32 +61,42 @@ export default function AudioEncodePage() { ErrorHandle(error); }, }); - - - const { mutate: startMutation } = useMutation({ - mutationFn: GetAudioEncodeStart, + // 转码任务 + const [codeLoadings, setCodeLoadings] = useState([]); + 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) => ( + + {text} + + ), + }, { title: "类型", dataIndex: "mode", @@ -156,16 +177,18 @@ export default function AudioEncodePage() { ), }, + { - title: "文件名称", - dataIndex: "file_name", + title: "时长", + dataIndex: "duration", align: "center", render: (text, record) => ( - {text} + {formatSecondsToHMS(text)} ), }, + { title: "状态", dataIndex: "encode_status", @@ -175,7 +198,16 @@ export default function AudioEncodePage() { {text == 0 && 未转码} {text == 1 && 转码成功} {text == 2 && 转码中} - {text == 3 && 转码失败} + {text == 3 && <> + 转码失败 + + + + + + { + setPagination({ ...pagination, name: value }); + }} + /> + + {/* 表格 */} + `共 ${total} 条`, + onChange: handleTableChange, + onShowSizeChange: handleTableChange, + }} + /> + + + + ); +} diff --git a/web/src/components/Filter.tsx b/web/src/components/Filter.tsx index 2cdd5be..6a5d2a8 100644 --- a/web/src/components/Filter.tsx +++ b/web/src/components/Filter.tsx @@ -67,7 +67,7 @@ const Filter: React.FC = ({ diff --git a/web/src/components/PlayerModal.tsx b/web/src/components/PlayerModal.tsx index 411cea2..996371d 100644 --- a/web/src/components/PlayerModal.tsx +++ b/web/src/components/PlayerModal.tsx @@ -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( 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) diff --git a/web/src/components/UploadAudio.tsx b/web/src/components/UploadAudio.tsx index 98ef636..f7e10ab 100644 --- a/web/src/components/UploadAudio.tsx +++ b/web/src/components/UploadAudio.tsx @@ -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: { diff --git a/web/src/components/channel/Channel.tsx b/web/src/components/channel/Channel.tsx index 251f8fc..d602e0e 100644 --- a/web/src/components/channel/Channel.tsx +++ b/web/src/components/channel/Channel.tsx @@ -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 = forwardRef(({onCallback},ref) => { +const ChannelModel: React.FC = 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(undefined); + const pid = useRef(0); + const audioName = useRef(''); const { ErrorHandle } = useGlobal(); const columns: ColumnsType = [ @@ -74,6 +77,25 @@ const ChannelModel: React.FC = forwardRef(({onCallback},ref) => { dataIndex: "protocol", render: (text: string) => text || "-", }, + { + title: "操作", + align: "center", + width: 120, + fixed: "right", + render: (_, record) => ( + + + + + + } + onCancel={onCancel} + // onOk={onCancel} >
@@ -158,7 +204,7 @@ const ChannelModel: React.FC = forwardRef(({onCallback},ref) => { onSearchChange={(value: string) => { setPagination({ ...pagination, name: value, bid: value }); }} - onSelectChange={(value: string) => { + onSelectChange={(value: any) => { setPagination({ ...pagination, status: value }); }} /> diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 41f737d..66bb58a 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -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["items"][number]; export default function Home() { @@ -14,12 +15,12 @@ export default function Home() { const items: MenuItem[] = [ { key: "sub0", - label: "推流任务", + label: "文件管理", icon: , }, { key: "sub1", - label: "基础配置", + label: "任务列表", icon: , }, ]; @@ -30,7 +31,7 @@ export default function Home() { return ( - {/*
+
-
*/} - {/*
*/} - + + + {/* */} {currentMenu == "sub0" && ( @@ -67,7 +68,7 @@ export default function Home() { )} {currentMenu == "sub1" && ( - 基础配置 + )} diff --git a/web/src/types/audiotask.ts b/web/src/types/audiotask.ts new file mode 100644 index 0000000..9a32bc0 --- /dev/null +++ b/web/src/types/audiotask.ts @@ -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[]; +}; \ No newline at end of file diff --git a/web/src/types/device.d.ts b/web/src/types/device.d.ts index f04eb56..125142d 100644 --- a/web/src/types/device.d.ts +++ b/web/src/types/device.d.ts @@ -189,7 +189,7 @@ type ChannelReq = { /** * true:在线; false:离线; */ - status?: string; + status?: boolean; }; export type ChannelRes = { diff --git a/web/src/utils/time.ts b/web/src/utils/time.ts new file mode 100644 index 0000000..2845360 --- /dev/null +++ b/web/src/utils/time.ts @@ -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}`; +} \ No newline at end of file