stream 播放支持选择 ffmpeg 配置
组件化 manage-database
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
@@ -19,6 +20,20 @@ type API struct {
|
|||||||
APIConfig APIConfig
|
APIConfig APIConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FfmpegConfigs struct {
|
||||||
|
FfmpegConfigs map[string]*FfmpegConfig `json:"ffmpeg_configs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddFfmpegConfigRequest struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
FfmpegConfig FfmpegConfig `json:"ffmpeg_config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FfmpegConfig struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Args string `json:"args"`
|
||||||
|
}
|
||||||
|
|
||||||
type Status struct {
|
type Status struct {
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -310,6 +325,12 @@ func (api *API) HandleGetFileStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
api.HandleErrorString(w, r, `parameter "id" should be an integer`)
|
api.HandleErrorString(w, r, `parameter "id" should be an integer`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
configs := q["config"]
|
||||||
|
if len(configs) == 0 {
|
||||||
|
api.HandleErrorString(w, r, `parameter "config" can't be empty`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configName := configs[0]
|
||||||
file, err := api.Db.GetFile(int64(id))
|
file, err := api.Db.GetFile(int64(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.HandleError(w, r, err)
|
api.HandleError(w, r, err)
|
||||||
@@ -322,16 +343,19 @@ func (api *API) HandleGetFileStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("[api] Stream file", path)
|
log.Println("[api] Stream file", path, configName)
|
||||||
|
|
||||||
cmd := exec.Command("ffmpeg",
|
ffmpegConfig, ok := api.APIConfig.FfmpegConfigs[configName]
|
||||||
"-i", path,
|
if !ok {
|
||||||
"-c:a", "libopus",
|
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
||||||
"-ab", "128k",
|
return
|
||||||
"-vn",
|
}
|
||||||
"-f", "matroska",
|
args := strings.Split(ffmpegConfig.Args, " ")
|
||||||
"-",
|
startArgs := []string {"-i", path}
|
||||||
)
|
endArgs := []string {"-vn", "-f", "matroska", "-"}
|
||||||
|
ffmpegArgs := append(startArgs, args...)
|
||||||
|
ffmpegArgs = append(ffmpegArgs, endArgs...)
|
||||||
|
cmd := exec.Command("ffmpeg", ffmpegArgs...)
|
||||||
cmd.Stdout = w
|
cmd.Stdout = w
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -409,10 +433,56 @@ func (api *API) HandleGetFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
io.Copy(w, src)
|
io.Copy(w, src)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleGetFfmpegConfigs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("[api] Get ffmpeg config list")
|
||||||
|
ffmpegConfigs:= &FfmpegConfigs{
|
||||||
|
FfmpegConfigs: api.APIConfig.FfmpegConfigs,
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(&ffmpegConfigs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleAddFfmpegConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
addFfmpegConfigRequest := AddFfmpegConfigRequest{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&addFfmpegConfigRequest)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check token
|
||||||
|
err = api.CheckToken(w, r, addFfmpegConfigRequest.Token)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check name and args not null
|
||||||
|
if addFfmpegConfigRequest.FfmpegConfig.Name == "" {
|
||||||
|
api.HandleErrorString(w, r, `"ffmpeg_config.name" can't be empty`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if addFfmpegConfigRequest.FfmpegConfig.Args == "" {
|
||||||
|
api.HandleErrorString(w, r, `"ffmpeg_config.args" can't be empty`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[api] Add ffmpeg config")
|
||||||
|
|
||||||
|
api.APIConfig.FfmpegConfigs[addFfmpegConfigRequest.FfmpegConfig.Name] = &addFfmpegConfigRequest.FfmpegConfig
|
||||||
|
api.HandleOK(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIConfig() (APIConfig) {
|
||||||
|
apiConfig := APIConfig{
|
||||||
|
FfmpegConfigs: make(map[string]*FfmpegConfig),
|
||||||
|
}
|
||||||
|
return apiConfig
|
||||||
|
}
|
||||||
|
|
||||||
type APIConfig struct {
|
type APIConfig struct {
|
||||||
DatabaseName string
|
DatabaseName string `json:"database_name"`
|
||||||
Addr string
|
Addr string `json:"addr"`
|
||||||
Token string
|
Token string `json:"token"`
|
||||||
|
FfmpegConfigs map[string]*FfmpegConfig `json:"ffmpeg_configs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPI(apiConfig APIConfig) (*API, error) {
|
func NewAPI(apiConfig APIConfig) (*API, error) {
|
||||||
@@ -444,9 +514,11 @@ func NewAPI(apiConfig APIConfig) (*API, error) {
|
|||||||
apiMux.HandleFunc("/get_files_in_folder", api.HandleGetFilesInFolder)
|
apiMux.HandleFunc("/get_files_in_folder", api.HandleGetFilesInFolder)
|
||||||
apiMux.HandleFunc("/get_random_files", api.HandleGetRandomFiles)
|
apiMux.HandleFunc("/get_random_files", api.HandleGetRandomFiles)
|
||||||
apiMux.HandleFunc("/get_file_stream", api.HandleGetFileStream)
|
apiMux.HandleFunc("/get_file_stream", api.HandleGetFileStream)
|
||||||
|
apiMux.HandleFunc("/get_ffmpeg_config_list", api.HandleGetFfmpegConfigs)
|
||||||
// below needs token
|
// below needs token
|
||||||
apiMux.HandleFunc("/walk", api.HandleWalk)
|
apiMux.HandleFunc("/walk", api.HandleWalk)
|
||||||
apiMux.HandleFunc("/reset", api.HandleReset)
|
apiMux.HandleFunc("/reset", api.HandleReset)
|
||||||
|
apiMux.HandleFunc("/add_ffmpeg_config", api.HandleAddFfmpegConfig)
|
||||||
|
|
||||||
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", apiMux))
|
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", apiMux))
|
||||||
mux.Handle("/web/", http.StripPrefix("/web", http.FileServer(http.Dir("web"))))
|
mux.Handle("/web/", http.StripPrefix("/web", http.FileServer(http.Dir("web"))))
|
||||||
|
|||||||
15
main.go
15
main.go
@@ -18,11 +18,18 @@ func init() {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
apiConfig := api.APIConfig{
|
apiConfig := api.NewAPIConfig()
|
||||||
DatabaseName: DatabaseName,
|
apiConfig.FfmpegConfigs["libopus 128k"] = &api.FfmpegConfig{
|
||||||
Addr: Listen,
|
Name: "libopus 128k",
|
||||||
Token: Token,
|
Args: "-c:a libopus -ab 128k",
|
||||||
}
|
}
|
||||||
|
apiConfig.FfmpegConfigs["libopus 256k"] = &api.FfmpegConfig{
|
||||||
|
Name: "libopus 256k",
|
||||||
|
Args: "-c:a libopus -ab 256k",
|
||||||
|
}
|
||||||
|
apiConfig.DatabaseName = DatabaseName
|
||||||
|
apiConfig.Addr = Listen
|
||||||
|
apiConfig.Token = Token
|
||||||
api, err := api.NewAPI(apiConfig)
|
api, err := api.NewAPI(apiConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<router-view @set_token="set_token" @play_audio="play_audio"></router-view>
|
<router-view :token="token" @set_token="set_token" @play_audio="play_audio"></router-view>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<component-audio-player :file=playing_audio_file></component-audio-player>
|
<component-audio-player :file=playing_audio_file></component-audio-player>
|
||||||
|
|||||||
75
web/index.js
75
web/index.js
@@ -149,10 +149,6 @@ const component_manage= {
|
|||||||
emits: ['set_token'],
|
emits: ['set_token'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
root: "",
|
|
||||||
pattern: [".flac", ".mp3"],
|
|
||||||
pattern_tmp: "",
|
|
||||||
s: "",
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
@@ -161,6 +157,22 @@ const component_manage= {
|
|||||||
<p>本站是 MSW Project 的一个应用,希望以个人之力分享被隐藏在历史中的音乐。</p>
|
<p>本站是 MSW Project 的一个应用,希望以个人之力分享被隐藏在历史中的音乐。</p>
|
||||||
<p>自己是V家厨,喜欢的p主包括 wonder-k, buzzG, *luna 等,但却因为种种原因淹没在主流音乐APP的曲库中。本站的初衷是为了让那些知名度低的 VOCALOID / ACG / 东方曲,能够被更多有缘人听到,同时有一个跨平台的工具,能够在低网速的条件下享受硬盘中的无损音乐。</p>
|
<p>自己是V家厨,喜欢的p主包括 wonder-k, buzzG, *luna 等,但却因为种种原因淹没在主流音乐APP的曲库中。本站的初衷是为了让那些知名度低的 VOCALOID / ACG / 东方曲,能够被更多有缘人听到,同时有一个跨平台的工具,能够在低网速的条件下享受硬盘中的无损音乐。</p>
|
||||||
<component-token :token="token" @set_token="$emit('set_token', $event)"></component-token>
|
<component-token :token="token" @set_token="$emit('set_token', $event)"></component-token>
|
||||||
|
<component-manage-database :token="token"></component-manage-database>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
const component_manage_database = {
|
||||||
|
props: ['token'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
root: "",
|
||||||
|
pattern: [".flac", ".mp3"],
|
||||||
|
pattern_tmp: "",
|
||||||
|
s: "",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -352,24 +364,33 @@ const component_audio_player = {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loop: true,
|
loop: true,
|
||||||
|
ffmpeg_config: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: ["file"],
|
props: ["file"],
|
||||||
template: `
|
template: `
|
||||||
|
<div>
|
||||||
<div v-if="computed_show">
|
<div v-if="computed_show">
|
||||||
<span>{{ file.filename }} / {{ file.foldername }}</span><br />
|
<span>{{ file.filename }} / {{ file.foldername }}</span><br />
|
||||||
<input type="checkbox" v-model="loop" />
|
<input type="checkbox" v-model="loop" />
|
||||||
<label>Loop</label><br />
|
<label>Loop</label><br />
|
||||||
<video class="audio-player" :src="computed_playing_audio_file_url" controls autoplay :loop="loop">
|
<video v-if="computed_show" class="audio-player" :src="computed_playing_audio_file_url" controls autoplay :loop="loop">
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
|
<component-stream-config @set_ffmpeg_config="set_ffmpeg_config"></component-stream-config>
|
||||||
|
</div>
|
||||||
`,
|
`,
|
||||||
|
methods: {
|
||||||
|
set_ffmpeg_config(ffmpeg_config) {
|
||||||
|
this.ffmpeg_config = ffmpeg_config
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
computed_playing_audio_file_url() {
|
computed_playing_audio_file_url() {
|
||||||
if (this.file.play_back_type === 'raw') {
|
if (this.file.play_back_type === 'raw') {
|
||||||
return '/api/v1/get_file_direct?id=' + this.file.id
|
return '/api/v1/get_file_direct?id=' + this.file.id
|
||||||
} else if (this.file.play_back_type === 'stream') {
|
} else if (this.file.play_back_type === 'stream') {
|
||||||
return '/api/v1/get_file_stream?id=' + this.file.id
|
return '/api/v1/get_file_stream?id=' + this.file.id + '&config=' + this.ffmpeg_config.name
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed_show() {
|
computed_show() {
|
||||||
@@ -480,6 +501,46 @@ const component_get_random_files = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const component_stream_config = {
|
||||||
|
emits: ['set_ffmpeg_config'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ffmpeg_config_list: [],
|
||||||
|
selected_ffmpeg_config: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<select v-model="selected_ffmpeg_config">
|
||||||
|
<option v-for="ffmpeg_config in ffmpeg_config_list" :value="ffmpeg_config">
|
||||||
|
{{ ffmpeg_config.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
mounted() {
|
||||||
|
axios.get('/api/v1/get_ffmpeg_config_list',
|
||||||
|
).then(response => {
|
||||||
|
var ffmpeg_configs = response.data.ffmpeg_configs
|
||||||
|
var tmp_list = []
|
||||||
|
for (var key in ffmpeg_configs) {
|
||||||
|
tmp_list.push(ffmpeg_configs[key])
|
||||||
|
}
|
||||||
|
tmp_list.sort()
|
||||||
|
this.ffmpeg_config_list = tmp_list
|
||||||
|
this.selected_ffmpeg_config = this.ffmpeg_config_list[0]
|
||||||
|
}).catch(err => {
|
||||||
|
this.ffmpeg_config_list = [{name: 'No avaliable config'}]
|
||||||
|
this.selected_ffmpeg_config = this.ffmpeg_config_list[0]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
selected_ffmpeg_config(n, o) {
|
||||||
|
this.$emit('set_ffmpeg_config', this.selected_ffmpeg_config)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', component: component_get_random_files},
|
{ path: '/', component: component_get_random_files},
|
||||||
{ path: '/search_files', component: component_search_files},
|
{ path: '/search_files', component: component_search_files},
|
||||||
@@ -517,6 +578,8 @@ app.component('component-search-files', component_search_files)
|
|||||||
app.component('component-get-random-files', component_get_random_files)
|
app.component('component-get-random-files', component_get_random_files)
|
||||||
app.component('component-file-dialog', component_file_dialog)
|
app.component('component-file-dialog', component_file_dialog)
|
||||||
app.component('component-token', component_token)
|
app.component('component-token', component_token)
|
||||||
|
app.component('component-stream-config', component_stream_config)
|
||||||
|
app.component('component-manage-database', component_manage_database)
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user