func (db *DB) list(ctx core.CommandContext) core.Result { //TODO args := ctx.Args var err error text, err := util.GetArg(args, "text", false, err) query, err := util.GetArg(args, "query", false, err) tags, err := util.GetArg(args, "tags", false, err) if text != nil { return core.ResultByError(core.ErrorNotImplemented) } if query != nil { res, err := db.ListQuery(*query) if err != nil { return core.ResultByError(errrs.New(err.Error())) } return core.Result{core.StatusOK, ItemToInterfaceSlice(res), err, false} } if tags != nil { return core.ResultByError(core.ErrorNotImplemented) } res, err := db.ListAll() if err != nil { return core.ResultByError(errrs.New(err.Error())) } return core.Result{core.StatusOK, ItemToInterfaceSlice(res), err, false} }
func (a *AS) LoginAS(ctx core.CommandContext) core.Result { var err error args := ctx.Args handshakeS, err := util.GetArg(args, "hs", true, err) handshake, err := util.CastBool(handshakeS, err) pVer, err := util.GetArg(args, "p", true, err) _, err = util.GetArg(args, "c", true, err) _, err = util.GetArg(args, "v", true, err) userS, err := util.GetArg(args, "u", true, err) timestampS, err := util.GetArg(args, "t", true, err) timestamp, err := util.CastInt64(timestampS, err) authMsg, err := util.GetArg(args, "a", true, err) if err != nil { return errorAS(StatusFailed, err) } if !*handshake { return errorAS(StatusFailed, errrs.New("this URL is only for handshake"+ " requests, hs must be true")) } if *pVer != "1.2.1" { return errorAS(StatusFailed, errrs.New("Protocol version must be 1.2.1")) } //TODO maybe check audioscrobbler client id and version // check timestamp timestampReal := time.Now().Unix() if util.Abs(timestampReal-*timestamp) > 120 { return errorAS(StatusBadTime, nil) } // get user user, err := a.db.GetUserByName(*userS) if user == nil { logger.Log.Println("incorrect auth from unknown user ", *userS) return errorAS(StatusBadAuth, nil) } // check auth // md5(md5(auth_token) + timestamp) md5Token := fmt.Sprintf("%x", md5.Sum([]byte(user.AuthToken))) correctStr := fmt.Sprintf("%s%d", md5Token, *timestamp) correctAuthMsg := fmt.Sprintf("%x", md5.Sum([]byte(correctStr))) if correctAuthMsg != *authMsg { logger.Log.Println("incorrect auth from", user.Name, "with", *authMsg, "instead of", correctAuthMsg) return errorAS(StatusBadAuth, nil) } return core.Result{StatusOK, []interface{}{user.AuthToken, a.nowPlayingURL, a.submissionURL}, nil, true} }
func ResultByError(err error) Result { if err == nil { return ResultOK } else { return Result{Status: StatusError, Error: errrs.New(err.Error())} } }
// Fetches an argument from an ArgMap, used for single args // Breaks and passes along the error given, if it is not nil. // If the argument does not exist, the return value is nil. // Parameter force can be used to return an error if the argument does not exist. func GetArg(args core.ArgMap, arg string, force bool, err error) (*string, error) { if err != nil { return nil, err } value, ok := args[arg] if !ok || len(value) == 0 { if force { return nil, errrs.New("Argument '" + arg + "' missing!") } return nil, nil } if len(value) > 1 { return nil, errrs.New("argument " + arg + " cannot be supplied more than once!") } return &value[0], nil }
func Load(configFile string) error { if loaded { return errrs.New("Config already loaded.") } loaded = true if _, err := os.Stat(configFile); os.IsNotExist(err) { log.Log.Println("Initializing new configuation.") return nil } b, err := ioutil.ReadFile(configFile) if err != nil { return err } var read Configuration err = json.Unmarshal(b, &read) if err != nil { return err } // convert loaded maps to their config structs for k := range read.Plugins { err := mapstructure.Decode(read.Plugins[k], Default.pluginTypes[k]) if err != nil { log.Log.Fatalln("Error loading config for plugin", k, "-", err) } read.Plugins[k] = Default.pluginTypes[k] } Current = &read //TODO ensure minimal config is done return nil }
func (s *Sync) Sync(targetPath string, convert bool) error { logger.Log.Println("getting data...") tracks, err := s.db.ListAll() if err != nil { return err } logger.Log.Println(len(tracks), "tracks found.") logger.Log.Println("indexing target...") cleanPath := path.Clean(targetPath) if cleanPath != "" { cleanPath += "/" } var targetFiles []file filepath.Walk(targetPath, func(filepath string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } filepath = path.Clean(filepath) if !strings.HasPrefix(filepath, cleanPath) { return errrs.New("file " + filepath + " is not within " + cleanPath) } filepath = strings.TrimPrefix(filepath, cleanPath) targetFiles = append(targetFiles, file{filepath, info}) return nil }) logger.Log.Println(len(targetFiles), "files in target") return core.ErrorNotImplemented }
// Checks a given authentication token agains the database. // On success, returns the permission level of the user and their ID // On failure, returns (AuthGuest, nil, error) func (db *DB) Authenticate(authtoken string) (core.AuthLevel, *int64, error) { invalidErr := errrs.New("auth token invalid") user := User{} err := db.db.Find(&user, "auth_token = ?", authtoken).Error if err != nil || user.Id == 0 { // not found log.Log.Println("authToken not found", err) return core.AuthGuest, nil, invalidErr } return user.AuthLevel, &user.Id, nil }
// Converts a *string to a boolean. // Passes along errrs, if not nil. func CastBool(arg *string, err error) (*bool, error) { if err != nil { return nil, err } if arg == nil { return nil, nil } casted, err := strconv.ParseBool(*arg) if err != nil { return nil, errrs.New(*arg + " is not boolean") } return &casted, nil }
// Converts a *string to a uint64. // Passes along errrs, if not nil. func CastInt64(arg *string, err error) (*int64, error) { if err != nil { return nil, err } if arg == nil { return nil, nil } casted, err := strconv.ParseInt(*arg, 10, 64) if err != nil { return nil, errrs.New(*arg + " is not an integer") } return &casted, nil }
// Converts a *string to a float32. // Passes along errrs, if not nil. func CastFloat(arg *string, err error) (*float32, error) { if err != nil { return nil, err } if arg == nil { return nil, nil } casted, err := strconv.ParseFloat(*arg, 32) if err != nil { return nil, errrs.New(*arg + " is not a floating point") } smaller := float32(casted) return &smaller, nil }
// Converts a *string to a uint. // Passes along errrs, if not nil. func CastUint(arg *string, err error) (*uint, error) { if err != nil { return nil, err } if arg == nil { return nil, nil } casted, err := strconv.ParseUint(*arg, 10, 32) if err != nil { return nil, errrs.New(*arg + " is not an integer") } smaller := uint(casted) return &smaller, nil }
func Save(configFile string) error { if Current == nil { return errrs.New("Config has not been initialized, cannot save!") } str, err := json.MarshalIndent(Current, "", " ") if err != nil { return err } err = ioutil.WriteFile(configFile, str, 0644) if err != nil { return err } return nil }
func (d *DB) Open(c core.Core) error { if d.open { return errrs.New("DB is already opened!") } d.c = c var err error file := config.Current.DbFile d.db, err = gorm.Open("sqlite3", file) if err != nil { return err } d.open = true d.db.LogMode(true) if err := d.checkTables(); err != nil { return err } err = d.checkSanity() if err != nil { //TODO what now? return err } d.initStats(c) d.initLogic(c) // hear the difference? d.initLogin(c) // it's subtle but it could save your life d.initList(c) c.RegisterCommand(core.Command{ []string{"rescan"}, "Refreshes the database by re-scanning the media folder.", map[string]string{}, core.AuthAdmin, func(_ core.CommandContext) core.Result { go d.Update() return core.ResultOK }}) return nil }
func (db *DB) Login(name string, password string) (success bool, authToken string, err error) { user := User{} err = db.db.Where("name = ?", name).Find(&user).Error if err != nil { return false, "", errrs.New(err.Error()) } if user.Id == 0 { return false, "", nil } salt := user.Password[:SaltSize] expected := user.Password[SaltSize:] hashedPassword := HashPassword([]byte(password), salt) if 1 == subtle.ConstantTimeCompare(expected, hashedPassword) { //TODO add option to create new authtoken to logout all clients return true, user.AuthToken, nil } return false, "", nil }
func (up *updater) step(file string, info os.FileInfo, err error) error { if info == nil || info.Name() == "." || info.Name() == ".." { return nil } if info.IsDir() && info.Name() == ".git" { return filepath.SkipDir } select { case sig, ok := <-up.job: if !ok || sig >= core.SignalTerminate { log.Log.Println("Terminate got, processing remaining files") return ErrorTerminate } default: } if info.IsDir() { //log.Log.Println("in", file) } else if linked, err := filepath.EvalSymlinks(file); err != nil || file != linked { if err != nil { log.Log.Println("Error walking files:", err) return nil } //TODO add loop detection err = filepath.Walk(linked, up.step) if err != nil { return err } } else if !strings.HasPrefix(info.Name(), ".") { select { case <-up.stopStepping: return errors.New("aborting") case up.allFiles <- entry{path.Dir(file), info.Name()}: } } return nil }
func (db *DB) CreateUser(name string, email string, authLevel core.AuthLevel, password string) (*User, error) { if !IsSafe(name, TypeUsername) { return nil, errrs.New(`Username is invalid. Can contain a-z, 0-9, underscore and dash, minimum 3 characters.`) } if !IsSafe(email, TypeEmail) { return nil, errrs.New("EMail is invalid.") } if !IsSafe(password, TypePassword) { return nil, errrs.New(`Password is invalid. Must be at least 6 and at most 128 characters.`) } // check for unique var num uint64 err := db.db.Table(UserTable).Where("name = ?", name).Count(&num).Error if err != nil { return nil, errrs.New("Error checking username: "******"User already exists!") } user := User{Name: name, Email: email, AuthLevel: authLevel} // create authtoken user.AuthToken, err = db.makeAuthToken() if err != nil { return nil, err } // hash password user.Password = MakePassword([]byte(password)) // store user err = db.db.Save(&user).Error if err != nil { return nil, errrs.New("Could not insert user: " + err.Error()) } return &user, nil }
SignalNone JobSignal = iota SignalTerminate SignalKill ) type Result struct { Status Status `json:"status"` Result interface{} `json:"result,omitempty"` Error error `json:"error,omitempty"` IsRaw bool `json:"-"` } var ( ResultOK = Result{Status: StatusOK, Result: nil, Error: nil} ErrorCmdNotFound = errrs.New("Command not found!") ErrorNotAllowed = errrs.New("You are not allowed to do that!") ErrorNotLoggedIn = errrs.New("You are not allowed to do that; you need to be logged in!") ErrorNotImplemented = errrs.New("Sorry, this feature is not implemented yet.") ErrorUserNotFound = errrs.New("User not found!") ErrorInvalidQuery = errrs.New("Invalid Query!") //ErrorItemNotFound = errrs.New("The requested item was not found.") ) func ResultByError(err error) Result { if err == nil { return ResultOK } else { return Result{Status: StatusError, Error: errrs.New(err.Error())} }
success chan bool stopStepping chan bool vlc *vlc.VLC // the receiving goroutine shall increment these numAllFiles int numNewFiles int numImportFiles int numInvalidFiles int numFailedFiles int numImportedFiles int } var Types = []string{"mp3", "wav", "flac", "m4a", "aac"} var ErrorTerminate error = errors.New("Terminating") func (d *DB) Update() { // keep file base up to date searchPath := config.Current.MediaPath if strings.Contains(searchPath, "~") { user, err := osuser.Current() if err != nil { log.Log.Println("Error getting user home directory:", err) return } searchPath = strings.Replace(searchPath, "~", user.HomeDir, -1) } if _, err := os.Stat(searchPath); os.IsNotExist(err) { log.Log.Println("Error: Music path", searchPath, "does not exist!") return
func (d *DB) Close() error { if !d.open { return errrs.New("DB is not open!") } return d.db.Close() }
func (p *IPod) Sync(mountpoint string) error { logger.Log.Println("getting data...") tracks, err := p.db.ListAll() if err != nil { return err } logger.Log.Println(len(tracks), "tracks found.") logger.Log.Println("indexing target...") ipodDb, err := gpod.New(mountpoint) if err != nil { return err } ipodTracks := ipodDb.Tracks() logger.Log.Println(len(ipodTracks), "tracks on iPod.") idxFuncGpod := func(t gpod.Track) string { return t.Title() + Seperator + t.Album() + Seperator + t.Artist() } idxFunc := func(t db.Item) string { str := t.Title + Seperator if t.Album.Valid { str += t.Album.String } return str + Seperator + t.Artist } idx := make(map[string]gpod.Track) for _, t := range ipodTracks { idx[idxFuncGpod(t)] = t } var tracksMissing []*db.Item // cfmedias db id -> gpod track tracksFound := make(map[int64]gpod.Track) var tracksUnmatched []gpod.Track matched := make(map[string]bool) for i := range tracks { t := &tracks[i] match, ok := idx[idxFunc(*t)] if !ok { tracksMissing = append(tracksMissing, t) continue } matched[idxFunc(*t)] = true tracksFound[t.Id] = match } for i := range ipodTracks { t := &ipodTracks[i] if !matched[idxFuncGpod(*t)] { tracksUnmatched = append(tracksUnmatched, *t) } } logger.Log.Println(len(tracksFound), "tracks found,", len(tracksUnmatched), "unknown tracks on iPod,", len(tracksMissing), "tracks to copy") m := map[string]interface{}{ "unmatched": tracksUnmatched, "missing": tracksMissing, } b, err := json.MarshalIndent(m, "", " ") if err != nil { return err } err = ioutil.WriteFile("sync-info.json", b, 0644) if err != nil { return err } logger.Log.Println("Wrote sync-info.json.") //TODO update tags // delete unmatched deleteErrors := make(map[gpod.Track]error) for _, t := range tracksUnmatched { logger.Log.Println("Deleting", t) err := t.Delete() if err != nil { logger.Log.Println("Error deleting", t, err) deleteErrors[t] = err continue } } // add missing copyErrors := make(map[*db.Item]error) for _, t := range tracksMissing { logger.Log.Println("Adding", t) ipodT := gpod.NewTrack(ipodDb) ipodT.SetTitle(t.Title) ipodT.SetAlbum(db.StrStr(t.Album)) ipodT.SetArtist(t.Artist) ipodT.SetGenre(db.StrStr(t.Genre)) //ipodT.SetFiletype(t.Filename) //ipodT.SetComment(t.Comment) //ipodT.SetComposer(t.Composer) //ipodT.SetDescription(t.Description) ipodT.SetAlbumartist(db.StrStr(t.AlbumArtist)) //ipodT.SetSize(t.Size) //ipodT.SetLength(t.Length) //TODO add missing fields // don't make sense yet //ipodT.SetRating(t.Rating()) //ipodT.SetPlaycount(t.Playcount) ipodT.SetTimeAdded(t.CreatedAt) //TODO playlist hierarchy path := t.Path() if path == nil { err := errrs.New("No file to copy") logger.Log.Println("Error finding", t, err) copyErrors[t] = err continue } ipodDb.Add(ipodT) ipodDb.MPL().Add(ipodT) err := ipodDb.Copy(ipodT, *path) if err != nil { logger.Log.Println("Error copying", t, err) copyErrors[t] = err ipodDb.MPL().Remove(ipodT) ipodDb.Remove(ipodT) } } err = ipodDb.Save() if err != nil { return err } // give back other errors, if any if len(copyErrors) > 0 { serr := &syncError{deleteErrors, copyErrors} return serr } return nil }
func (p *IPod) Sync(mountpoint string) error { return errrs.New("iPod sync not supported on Windows. I'm sorry.") }
func (db *DB) initLogin(c core.Core) { c.RegisterCommand(core.Command{ []string{"create_user"}, "Creates a user in the database", map[string]string{ "name": "Username", "email": "E-Mail", "auth_level": fmt.Sprintf("User Rank: Guest(%d), User(%d), "+ "Admin(%d), Root(%d)", core.AuthGuest, core.AuthUser, core.AuthAdmin, core.AuthRoot), "password": "******"}, core.AuthAdmin, func(ctx core.CommandContext) core.Result { args := ctx.Args var err error name, err := util.GetArg(args, "name", true, err) email, err := util.GetArg(args, "email", true, err) authLevelS, err := util.GetArg(args, "auth_level", true, err) password, err := util.GetArg(args, "password", true, err) authLevelI, err := util.CastUint(authLevelS, err) if err != nil { return core.Result{Status: core.StatusError, Error: err} } authLevel := core.AuthLevel(*authLevelI) if authLevel >= ctx.AuthLevel { return core.ResultByError(errrs.New("You cannot create a user" + " with that level!")) } user, err := db.CreateUser(*name, *email, authLevel, *password) if err == nil { return core.Result{Status: core.StatusOK, Result: user} } return core.Result{Status: core.StatusError, Error: err} }}) c.RegisterCommand(core.Command{ []string{"login"}, "Logs in with user/password and returns the auth token", map[string]string{ "name": "Username", "password": "******"}, core.AuthGuest, func(ctx core.CommandContext) core.Result { args := ctx.Args var err error name, err := util.GetArg(args, "name", true, err) password, err := util.GetArg(args, "password", true, err) if err != nil { return core.ResultByError(err) } success, authToken, err := db.Login(*name, *password) if err == nil && success { return core.Result{Status: core.StatusOK, Result: map[string]string{"auth_token": authToken}} } if err == nil { return core.Result{Status: core.StatusError, Error: errrs.New("Wrong username or password")} } return core.Result{Status: core.StatusError, Error: err} }}) c.RegisterCommand(core.Command{ []string{"change_authtoken", "logout"}, `Changes the authentication token of the user, thereby logging out all clients`, map[string]string{ "name": `(Optional) username of the user to log out. Leave empty to use current user`, }, core.AuthUser, func(ctx core.CommandContext) core.Result { args := ctx.Args var err error name, err := util.GetArg(args, "name", false, err) if err != nil { return core.ResultByError(err) } var user *User if name != nil { if ctx.AuthLevel < core.AuthAdmin { return core.ResultByError(core.ErrorNotAllowed) } user, err = db.GetUserByName(*name) } else { if ctx.UserId == nil { return core.ResultByError(core.ErrorNotLoggedIn) } user, err = db.GetUser(*ctx.UserId) } if user == nil { return core.ResultByError(core.ErrorUserNotFound) } _, err = db.ChangeAuthToken(user) return core.ResultByError(err) }}) }