// // rhimportd // // The Radio Helsinki Rivendell Import Daemon // // // Copyright (C) 2015-2016 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 ( "fmt" "io/ioutil" "log" "net/http" "os" "path" "code.helsinki.at/rhrd-go/rddb" "github.com/andelf/go-curl" ) const ( CART_MAX = 999999 CUT_MAX = 999 // not sure if rdxport.cgi can handle filesizes > MAX(INT32) FILESIZE_MAX = (2 * 1024 * 1024 * 1024) - 1 // TODO: make this configurable ARCHIV_HOST = "bigmama.helsinki.at" ARCHIV_USER = "rhimport" ARCHIV_BASE_PATH = "/srv/_nfs4_root_/archiv/ogg-flac" ) var ( bool2str = map[bool]string{false: "0", true: "1"} ) func Init(stdlog, dbglog *log.Logger) { curl.GlobalInit(curl.GLOBAL_ALL) fetcherInit(stdlog, dbglog) } type ProgressCB func(step int, stepName string, current, total float64, title string, cart, cut uint, userdata interface{}) bool type DoneCB func(result Result, userdata interface{}) bool type ProgressData struct { Step int StepName string Current float64 Total float64 Title string Cart uint Cut uint } type Result struct { ResponseCode int ErrorString string Cart uint Cut uint SourceFile string } type FilePolicy int const ( Delete FilePolicy = iota Keep ) func (p *FilePolicy) String() string { switch *p { case Delete: return "delete" case Keep: return "keep" } return "unknown" } func (p *FilePolicy) FromString(str string) error { switch str { case "delete": *p = Delete case "keep": *p = Keep default: return fmt.Errorf("must be on of: auto, keep, delete") } return nil } type AttachmentChunk struct { Data []byte Error error } type Context struct { conf *Config db *rddb.DB stdlog *log.Logger dbglog *log.Logger UserName string Password string Trusted bool ShowId uint ClearShowCarts bool ShowCarts []uint GroupName string Cart uint ClearCart bool Cut uint Channels uint NormalizationLevel int AutotrimLevel int UseMetaData bool SourceUri string AttachmentChan chan AttachmentChunk FetchConverter string ExtraMetaData map[string]string OrigFilename string Title string WorkDir string SourceFile string SourceFilePolicy FilePolicy LoudnessCorr float64 ProgressCallBack ProgressCB ProgressCallBackData interface{} Cancel <-chan bool } func NewContext(conf *Config, db *rddb.DB, stdlog, dbglog *log.Logger) *Context { if stdlog == nil { stdlog = log.New(ioutil.Discard, "", 0) } if dbglog == nil { dbglog = log.New(ioutil.Discard, "", 0) } ctx := &Context{} ctx.conf = conf ctx.db = db ctx.stdlog = stdlog ctx.dbglog = dbglog ctx.UserName = "" ctx.Password = "" ctx.Trusted = false ctx.ShowId = 0 ctx.ClearShowCarts = false ctx.GroupName = "" ctx.Cart = 0 ctx.ClearCart = false ctx.Cut = 0 ctx.Channels = conf.ImportParamDefaults.Channels ctx.NormalizationLevel = conf.ImportParamDefaults.NormalizationLevel ctx.AutotrimLevel = conf.ImportParamDefaults.AutotrimLevel ctx.UseMetaData = conf.ImportParamDefaults.UseMetaData ctx.AttachmentChan = make(chan AttachmentChunk, 32) ctx.FetchConverter = "ffmpeg-bs1770" ctx.ExtraMetaData = make(map[string]string) ctx.OrigFilename = "" ctx.Title = "" ctx.SourceFile = "" ctx.LoudnessCorr = 0.0 ctx.SourceFilePolicy = Delete ctx.ProgressCallBack = nil ctx.Cancel = nil return ctx } func (ctx *Context) 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.WorkDir == "" { return fmt.Errorf("empty WorkDir is not allowed") } if ctx.ShowId != 0 { if ctx.ShowId != 0 && ctx.ShowId > CART_MAX { return fmt.Errorf("ShowId %d is outside of allowed range (0 < show-id < %d)", ctx.ShowId, CART_MAX) } if err := ctx.getShowInfo(); err != nil { return err } if ctx.Cart == 0 { return nil } if ctx.Cart > CART_MAX { return fmt.Errorf("Cart %d is outside of allowed range (0 < cart < %d)", ctx.Cart, CART_MAX) } for _, cart := range ctx.ShowCarts { if cart == ctx.Cart { return nil } } return fmt.Errorf("Cart %d is not part of the show with show-id %d", ctx.Cart, ctx.ShowId) } if ctx.GroupName != "" { ismusic, err := ctx.checkMusicGroup() if err != nil { return err } if !ismusic { return fmt.Errorf("supplied GroupName '%s' is not a music pool", ctx.GroupName) } if ctx.Cart != 0 || ctx.Cut != 0 { return fmt.Errorf("Cart and Cut must not be supplied when importing into a music group") } return ctx.getMusicInfo() } if ctx.Cart == 0 { return fmt.Errorf("either ShowId, PoolName or CartNumber must be supplied") } if ctx.Cart > CART_MAX { return fmt.Errorf("Cart %d is outside of allowed range (0 < cart < %d)", ctx.Cart, CART_MAX) } if ctx.Cut != 0 && ctx.Cut > CUT_MAX { return fmt.Errorf("Cut %d is outside of allowed range (0 < cart < %d)", ctx.Cut, CUT_MAX) } if ctx.Channels != 1 && ctx.Channels != 2 { return fmt.Errorf("channles must be 1 or 2") } return nil } func (ctx *Context) CreateTempWorkDir() (err error) { ctx.WorkDir, err = ioutil.TempDir(ctx.conf.TempDir, "rhimport-") return } func (ctx *Context) SwitchTempWorkDir(newDir string) { ctx.RemoveTempWorkDir() ctx.WorkDir = newDir return } func (ctx *Context) RemoveTempWorkDir() { if err := os.RemoveAll(ctx.WorkDir); err != nil { ctx.stdlog.Printf("Error removing WorkDir: %s", err) } return } func (ctx *Context) getPassword(cached bool) (err error) { ctx.Password, err = ctx.db.GetPassword(ctx.UserName, cached) return } func (ctx *Context) CheckPassword() (bool, error) { return ctx.db.CheckPassword(ctx.UserName, ctx.Password) } func (ctx *Context) getGroupOfCart() (err error) { ctx.GroupName, err = ctx.db.GetGroupOfCart(ctx.Cart) return } func (ctx *Context) getShowInfo() (err error) { ctx.GroupName, ctx.NormalizationLevel, ctx.AutotrimLevel, ctx.ShowCarts, err = ctx.db.GetShowInfo(ctx.ShowId) ctx.Channels = 2 ctx.UseMetaData = true return } func (ctx *Context) checkMusicGroup() (bool, error) { return ctx.db.CheckMusicGroup(ctx.GroupName) } func (ctx *Context) getMusicInfo() (err error) { ctx.NormalizationLevel, ctx.AutotrimLevel, err = ctx.db.GetMusicInfo(ctx.GroupName) ctx.Channels = 2 ctx.UseMetaData = true ctx.Cart = 0 ctx.Cut = 0 return } func (ctx *Context) updateCutCartTitle() (err error) { filename := ctx.OrigFilename if filename == "" { filename = ctx.SourceFile } return ctx.db.UpdateCutCartTitle(ctx.Cart, ctx.Cut, ctx.GroupName, filename) } func (ctx *Context) reportProgress(step int, stepName string, current, total float64) { if ctx.ProgressCallBack != nil { title := ctx.Title if title == "" { title = path.Base(ctx.OrigFilename) if title == "" || title == "." { title = path.Base(ctx.SourceFile) } } if keep := ctx.ProgressCallBack(step, stepName, current, total, title, ctx.Cart, ctx.Cut, ctx.ProgressCallBackData); !keep { ctx.ProgressCallBack = nil } } } func (ctx *Context) isCanceled() bool { return ctx.Cancel != nil && len(ctx.Cancel) > 0 } func RemoveCart(cart uint, rdxportEndpoint, username, password string) (err error) { conf := &Config{} conf.RDXportEndpoint = rdxportEndpoint ctx := NewContext(conf, nil, nil, nil) ctx.Cart = cart ctx.UserName = username ctx.Password = password res := &Result{ResponseCode: http.StatusOK} if err = removeCart(ctx, res); err != nil { return err } if res.ResponseCode != http.StatusOK { return fmt.Errorf("Error removing cart %d: %s", cart, res.ErrorString) } return nil }