// // 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 != "" { ismusic, err := ctx.checkMusicGroup() if err != nil { return err } if !ismusic { return fmt.Errorf("supplied GroupName is not a music pool") } 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 { return res.err } ctx.Password = res.password return nil } func (ctx *ImportContext) getGroupOfCart() error { req := getGroupOfCartRequest{} req.cart = ctx.Cart req.response = make(chan getGroupOfCartResult) ctx.RdDb.getGroupOfCartChan <- req res := <-req.response if res.err != nil { return res.err } ctx.GroupName = res.group return nil } 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("Show %d:\n", ctx.ShowId) rhdl.Printf(" Title: '%s'\n", res.title) rhdl.Printf(" Normalization Level: %d\n", res.norm_lvl) rhdl.Printf(" Autotrim Level: %d\n", res.trim_lvl) rhdl.Printf(" Carts: %v\n", res.carts) return } func (ctx *ImportContext) checkMusicGroup() (bool, error) { req := checkMusicGroupRequest{} req.group = ctx.GroupName req.response = make(chan checkMusicGroupResult) ctx.RdDb.checkMusicGroupChan <- req res := <-req.response if res.err != nil { return false, res.err } return res.ismusic, nil } func (ctx *ImportContext) getMusicInfo() (err error) { req := getMusicInfoRequest{} req.group = ctx.GroupName req.response = make(chan getMusicInfoResult) ctx.RdDb.getMusicInfoChan <- req res := <-req.response if res.err != nil { return res.err } ctx.NormalizationLevel = res.norm_lvl ctx.AutotrimLevel = res.trim_lvl 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 { err = fmt.Errorf("importer: %s", err) 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 (ctx.Cart not in cartlist) -> Error // - if (ClearShowCarts == true): foreach(cart in cartlist): remove_cart(cart) [200 || 404 -> OK] // - add_cart(ctx, res) [200 -> OK] // - add_cut(ctx, res) [200 -> OK] return } else if ctx.GroupName != "" { res.ResponseCode = 500 res.ErrorString = "Importing to music pools is not yet implemented" // TODO: fetch info from dropboxes (import-params) // - add_cart(ctx, res) [200 -> OK] // - add_cut(ctx, res) [200 -> OK] return } 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] return } 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 }