// // rhimportd // // The Radio Helsinki Rivendell Import Daemon // // // Copyright (C) 2015 Christian Pointner // // This file is part of rhimportd. // // rhimportd is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // // rhimportd is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with rhimportd. If not, see . // package rhimport import ( "bufio" "bytes" "fmt" "github.com/golang-basic/go-curl" "mime/multipart" "net/http" "os" "path" ) var ( bool2str = map[bool]string{false: "0", true: "1"} ) type ImportContext struct { *Config *RdDb UserName string Password string Trusted bool ShowId int ClearShowCarts bool GroupName string Cart uint Cut uint Channels uint NormalizationLevel int AutotrimLevel int UseMetaData bool SourceUri string SourceFile string DeleteSourceFile bool DeleteSourceDir bool ProgressCallBack func(step int, step_name string, progress float64, userdata interface{}) ProgressCallBackData interface{} } func NewImportContext(conf *Config, rddb *RdDb, user string) *ImportContext { ctx := new(ImportContext) ctx.Config = conf ctx.RdDb = rddb ctx.UserName = user ctx.Password = "" ctx.Trusted = false ctx.ShowId = 0 ctx.ClearShowCarts = false ctx.GroupName = "" ctx.Cart = 0 ctx.Cut = 0 ctx.Channels = 2 ctx.NormalizationLevel = -12 ctx.AutotrimLevel = 0 ctx.UseMetaData = false ctx.SourceFile = "" ctx.DeleteSourceFile = false ctx.DeleteSourceDir = false ctx.ProgressCallBack = nil return ctx } func (ctx *ImportContext) SanityCheck() error { if ctx.UserName == "" { return fmt.Errorf("empty Username is not allowed") } if ctx.Password == "" && !ctx.Trusted { return fmt.Errorf("empty Password on untrusted control interface is not allowed") } if ctx.ShowId != 0 { return nil } if ctx.GroupName != "" { // TODO: check if GroupName is a music pool -> Error if not return nil } if ctx.Cart == 0 { return fmt.Errorf("either ShowId, PoolName or CartNumber must be supplied") } if ctx.Channels != 1 && ctx.Channels != 2 { return fmt.Errorf("channles must be 1 or 2") } return nil } func (ctx *ImportContext) getPassword(cached bool) (err error) { req := getPasswordRequest{} req.username = ctx.UserName req.cached = cached req.response = make(chan getPasswordResult) ctx.RdDb.getPasswordChan <- req res := <-req.response if res.err != nil { err = res.err return } ctx.Password = res.password return } func (ctx *ImportContext) getShowInfo() (err error) { req := getShowInfoRequest{} req.showid = ctx.ShowId req.response = make(chan getShowInfoResult) ctx.RdDb.getShowInfoChan <- req res := <-req.response if res.err != nil { err = res.err return } rhdl.Printf("Title of show %d is '%s'", ctx.ShowId, res.title) return } type ImportResult struct { ResponseCode int ErrorString string Cart uint Cut uint } func (self *ImportResult) fromRDWebResult(rdres *RDWebResult) { self.ResponseCode = rdres.ResponseCode self.ErrorString = rdres.ErrorString if rdres.AudioConvertError != 0 { self.ErrorString += fmt.Sprint(", Audio Convert Error: %d", rdres.AudioConvertError) } } func add_cart(ctx *ImportContext, result *ImportResult) (err error) { var b bytes.Buffer w := multipart.NewWriter(&b) if err = w.WriteField("COMMAND", "12"); err != nil { return } if err = w.WriteField("LOGIN_NAME", ctx.UserName); err != nil { return } if err = w.WriteField("PASSWORD", ctx.Password); err != nil { return } if err = w.WriteField("GRPOUP_NAME", ctx.GroupName); err != nil { return } if err = w.WriteField("TYPE", "audio"); err != nil { return } if ctx.Cart != 0 { if err = w.WriteField("CART_NUMBER", fmt.Sprintf("%d", ctx.Cart)); err != nil { return } } w.Close() var res *http.Response if res, err = send_post_request(ctx.Config.RDXportEndpoint, &b, w.FormDataContentType()); err != nil { return } defer res.Body.Close() if res.StatusCode != http.StatusOK { var rdres *RDWebResult if rdres, err = NewRDWebResultFromXML(res.Body); err != nil { return } result.fromRDWebResult(rdres) return } var cartadd *RDCartAdd if cartadd, err = NewRDCartAddFromXML(res.Body); err != nil { return } result.ResponseCode = res.StatusCode result.ErrorString = "OK" result.Cart = cartadd.Carts[0].Number ctx.Cart = result.Cart return } func add_cut(ctx *ImportContext, result *ImportResult) (err error) { var b bytes.Buffer w := multipart.NewWriter(&b) if err = w.WriteField("COMMAND", "10"); err != nil { return } if err = w.WriteField("LOGIN_NAME", ctx.UserName); err != nil { return } if err = w.WriteField("PASSWORD", ctx.Password); err != nil { return } if err = w.WriteField("CART_NUMBER", fmt.Sprintf("%d", ctx.Cart)); err != nil { return } w.Close() var res *http.Response if res, err = send_post_request(ctx.Config.RDXportEndpoint, &b, w.FormDataContentType()); err != nil { return } defer res.Body.Close() if res.StatusCode != http.StatusOK { var rdres *RDWebResult if rdres, err = NewRDWebResultFromXML(res.Body); err != nil { return } result.fromRDWebResult(rdres) return } var cutadd *RDCutAdd if cutadd, err = NewRDCutAddFromXML(res.Body); err != nil { return } result.ResponseCode = res.StatusCode result.ErrorString = "OK" result.Cut = cutadd.Cuts[0].Number ctx.Cut = cutadd.Cuts[0].Number return } func remove_cart(ctx *ImportContext, result *ImportResult) (err error) { var b bytes.Buffer w := multipart.NewWriter(&b) if err = w.WriteField("COMMAND", "13"); err != nil { return } if err = w.WriteField("LOGIN_NAME", ctx.UserName); err != nil { return } if err = w.WriteField("PASSWORD", ctx.Password); err != nil { return } if err = w.WriteField("CART_NUMBER", fmt.Sprintf("%d", ctx.Cart)); err != nil { return } w.Close() var res *http.Response if res, err = send_post_request(ctx.Config.RDXportEndpoint, &b, w.FormDataContentType()); err != nil { return } defer res.Body.Close() var rdres *RDWebResult if rdres, err = NewRDWebResultFromXML(res.Body); err != nil { return } result.fromRDWebResult(rdres) return } func send_post_request(url string, b *bytes.Buffer, contenttype string) (res *http.Response, err error) { var req *http.Request if req, err = http.NewRequest("POST", url, b); err != nil { return } if contenttype != "" { req.Header.Set("Content-Type", contenttype) } client := &http.Client{} if res, err = client.Do(req); err != nil { return } return } func import_audio_create_request(ctx *ImportContext, easy *curl.CURL) (form *curl.Form, err error) { form = curl.NewForm() if err = form.Add("COMMAND", "2"); err != nil { return } if err = form.Add("LOGIN_NAME", ctx.UserName); err != nil { return } if err = form.Add("PASSWORD", ctx.Password); err != nil { return } if err = form.Add("CART_NUMBER", fmt.Sprintf("%d", ctx.Cart)); err != nil { return } if err = form.Add("CUT_NUMBER", fmt.Sprintf("%d", ctx.Cut)); err != nil { return } if err = form.Add("CHANNELS", fmt.Sprintf("%d", ctx.Channels)); err != nil { return } if err = form.Add("NORMALIZATION_LEVEL", fmt.Sprintf("%d", ctx.NormalizationLevel)); err != nil { return } if err = form.Add("AUTOTRIM_LEVEL", fmt.Sprintf("%d", ctx.AutotrimLevel)); err != nil { return } if err = form.Add("USE_METADATA", bool2str[ctx.UseMetaData]); err != nil { return } if err = form.AddFile("FILENAME", ctx.SourceFile); err != nil { return } return } func import_audio(ctx *ImportContext, result *ImportResult) (err error) { easy := curl.EasyInit() if easy != nil { defer easy.Cleanup() easy.Setopt(curl.OPT_URL, ctx.Config.RDXportEndpoint) easy.Setopt(curl.OPT_POST, true) var form *curl.Form if form, err = import_audio_create_request(ctx, easy); err != nil { return } easy.Setopt(curl.OPT_HTTPPOST, form) easy.Setopt(curl.OPT_HTTPHEADER, []string{"Expect:"}) var resbody bytes.Buffer easy.Setopt(curl.OPT_WRITEFUNCTION, func(ptr []byte, userdata interface{}) bool { b := userdata.(*bytes.Buffer) b.Write(ptr) return true }) easy.Setopt(curl.OPT_WRITEDATA, &resbody) easy.Setopt(curl.OPT_NOPROGRESS, false) easy.Setopt(curl.OPT_PROGRESSFUNCTION, func(dltotal, dlnow, ultotal, ulnow float64, userdata interface{}) bool { ctx := userdata.(*ImportContext) if ctx.ProgressCallBack != nil { ctx.ProgressCallBack(2, "importing", ulnow/ultotal, ctx.ProgressCallBackData) } return true }) easy.Setopt(curl.OPT_PROGRESSDATA, ctx) if err = easy.Perform(); err != nil { return } // var status_code interface{} // if status_code, err = easy.Getinfo(curl.INFO_RESPONSE_CODE); err != nil { // return // } var rdres *RDWebResult if rdres, err = NewRDWebResultFromXML(bufio.NewReader(&resbody)); err != nil { return } result.fromRDWebResult(rdres) return } else { err = fmt.Errorf("Error initializing libcurl") } return } func cleanup_files(ctx *ImportContext) { if ctx.DeleteSourceFile { rhdl.Printf("importer: removing file: %s", ctx.SourceFile) if err := os.Remove(ctx.SourceFile); err != nil { rhl.Printf("importer: error removing source file: %s", err) return } if ctx.DeleteSourceDir { dir := path.Dir(ctx.SourceFile) rhdl.Printf("importer: also removing directory: %s", dir) if err := os.Remove(dir); err != nil { rhl.Printf("importer: error removing source directory: %s", err) } } } return } func ImportFile(ctx *ImportContext) (res *ImportResult, err error) { rhl.Println("ImportFile called for", ctx.SourceFile) defer cleanup_files(ctx) if ctx.ProgressCallBack != nil { ctx.ProgressCallBack(2, "importing", 0.0, ctx.ProgressCallBackData) } if ctx.Trusted { if err = ctx.getPassword(true); err != nil { return } } res = &ImportResult{} if ctx.ShowId != 0 { res.ResponseCode = 500 res.ErrorString = "Importing to shows using the show-id is not yet implemented" // TODO: fetch info from dropboxes (cartlist, groupname, import-params) // - if (ClearShowCarts == true): foreach(cart in cartlist): remove_cart(cart) [200 || 404 -> OK] // - ctx.cart = 0 // - add_cart(ctx, res) [200 -> OK] // - add_cut(ctx, res) [200 -> OK] // - import_audio(ctx, res) [200 -> OK] return } else if ctx.GroupName != "" { res.ResponseCode = 500 res.ErrorString = "Importing to music pools using the group name is not yet implemented" // TODO: fetch info from dropboxes (import-params) // - add_cart(ctx, res) [200 -> OK] // - add_cut(ctx, res) [200 -> OK] // - import_audio(ctx, res) [200 -> OK] } else if ctx.Cart != 0 && ctx.Cut == 0 { res.ResponseCode = 500 res.ErrorString = "Importing to a Cart which might not exist is not yet implemented" // TODO: (everything except Cut, GroupName and ShowId must be in context) // - ctx.GetGroupOfCart(ctx.Cart) // - remove_cart(ctx, res) [200 || 404 -> OK] // - add_cart(ctx, res) [200 -> OK] // - add_cut(ctx, res) [200 -> OK] // - import_audio(ctx, res) [200 -> OK] } else if ctx.Cart != 0 && ctx.Cut != 0 { if err = import_audio(ctx, res); err != nil { return } res.Cart = ctx.Cart res.Cut = ctx.Cut } else { res.ResponseCode = http.StatusBadRequest res.ErrorString = "The request doesn't contain enough Information to be processed" } rhdl.Printf("ImportResult: %+v\n", res) if res.ResponseCode == http.StatusOK { rhl.Println("ImportFile succesfully imported", ctx.SourceFile) } else { rhl.Println("ImportFile import of", ctx.SourceFile, "was unsuccesful") } return }