diff --git a/server/ctrladmin/ctrl.go b/server/ctrladmin/ctrl.go index 1d542e4..f5557ce 100644 --- a/server/ctrladmin/ctrl.go +++ b/server/ctrladmin/ctrl.go @@ -236,6 +236,7 @@ type Flash struct { Type FlashType } +//nolint:gochecknoinits func init() { gob.Register(&Flash{}) } diff --git a/server/ctrladmin/handlers.go b/server/ctrladmin/handlers.go index 999dc53..ce11fb8 100644 --- a/server/ctrladmin/handlers.go +++ b/server/ctrladmin/handlers.go @@ -82,7 +82,7 @@ func (c *Controller) ServeHome(r *http.Request) *Response { c.DB. Where("user_id=?", user.ID). Find(&data.TranscodePreferences) - for profile := range encode.Profiles { + for profile := range encode.Profiles() { data.TranscodeProfiles = append(data.TranscodeProfiles, profile) } // diff --git a/server/ctrlsubsonic/handlers_raw.go b/server/ctrlsubsonic/handlers_raw.go index 18fdc2d..fcb9893 100644 --- a/server/ctrlsubsonic/handlers_raw.go +++ b/server/ctrlsubsonic/handlers_raw.go @@ -72,7 +72,7 @@ func serveTrackRaw(w http.ResponseWriter, r *http.Request, opts serveTrackOption } func serveTrackEncode(w http.ResponseWriter, r *http.Request, opts serveTrackOptions) { - profile := encode.Profiles[opts.pref.Profile] + profile := encode.Profiles()[opts.pref.Profile] bitrate := encode.GetBitrate(opts.maxBitrate, profile) trackPath := path.Join(opts.musicPath, opts.track.RelPath()) cacheKey := encode.CacheKey(trackPath, opts.pref.Profile, bitrate) diff --git a/server/ctrlsubsonic/spec/spec.go b/server/ctrlsubsonic/spec/spec.go index 7b8c93b..d652bb5 100644 --- a/server/ctrlsubsonic/spec/spec.go +++ b/server/ctrlsubsonic/spec/spec.go @@ -7,7 +7,7 @@ import ( "go.senan.xyz/gonic/version" ) -var ( +const ( apiVersion = "1.9.0" xmlns = "http://subsonic.org/restapi" ) diff --git a/server/db/db.go b/server/db/db.go index 634f30c..f710400 100644 --- a/server/db/db.go +++ b/server/db/db.go @@ -31,9 +31,8 @@ func wrapMigrations(migrs ...gormigrate.Migration) []*gormigrate.Migration { return ret } -var ( - dbMaxOpenConns = 1 - dbOptions = url.Values{ +func defaultOptions() url.Values { + return url.Values{ // with this, multiple connections share a single data and schema cache. // see https://www.sqlite.org/sharedcache.html "cache": {"shared"}, @@ -43,7 +42,7 @@ var ( "_journal_mode": {"WAL"}, "_foreign_keys": {"true"}, } -) +} type DB struct { *gorm.DB @@ -55,13 +54,13 @@ func New(path string) (*DB, error) { Scheme: "file", Opaque: path, } - url.RawQuery = dbOptions.Encode() + url.RawQuery = defaultOptions().Encode() db, err := gorm.Open("sqlite3", url.String()) if err != nil { return nil, fmt.Errorf("with gorm: %w", err) } db.SetLogger(log.New(os.Stdout, "gorm ", 0)) - db.DB().SetMaxOpenConns(dbMaxOpenConns) + db.DB().SetMaxOpenConns(1) migrOptions := &gormigrate.Options{ TableName: "migrations", IDColumnName: "id", @@ -69,13 +68,13 @@ func New(path string) (*DB, error) { UseTransaction: false, } migr := gormigrate.New(db, migrOptions, wrapMigrations( - migrationInitSchema, - migrationCreateInitUser, - migrationMergePlaylist, - migrationCreateTranscode, - migrationAddGenre, - migrationUpdateTranscodePrefIDX, - migrationAddAlbumIDX, + migrateInitSchema(), + migrateCreateInitUser(), + migrateMergePlaylist(), + migrateCreateTranscode(), + migrateAddGenre(), + migrateUpdateTranscodePrefIDX(), + migrateAddAlbumIDX(), )) if err = migr.Migrate(); err != nil { return nil, fmt.Errorf("migrating to latest version: %w", err) diff --git a/server/db/migrations.go b/server/db/migrations.go index b777777..609762c 100644 --- a/server/db/migrations.go +++ b/server/db/migrations.go @@ -9,55 +9,58 @@ import ( // $ date '+%Y%m%d%H%M' -// not really a migration -var migrationInitSchema = gormigrate.Migration{ - ID: "202002192100", - Migrate: func(tx *gorm.DB) error { - return tx.AutoMigrate( - Artist{}, - Track{}, - User{}, - Setting{}, - Play{}, - Album{}, - Playlist{}, - PlayQueue{}, - ). - Error - }, +func migrateInitSchema() gormigrate.Migration { + return gormigrate.Migration{ + ID: "202002192100", + Migrate: func(tx *gorm.DB) error { + return tx.AutoMigrate( + Artist{}, + Track{}, + User{}, + Setting{}, + Play{}, + Album{}, + Playlist{}, + PlayQueue{}, + ). + Error + }, + } } -// not really a migration -var migrationCreateInitUser = gormigrate.Migration{ - ID: "202002192019", - Migrate: func(tx *gorm.DB) error { - const ( - initUsername = "admin" - initPassword = "admin" - ) - err := tx. - Where("name=?", initUsername). - First(&User{}). - Error - if !gorm.IsRecordNotFoundError(err) { - return nil - } - return tx.Create(&User{ - Name: initUsername, - Password: initPassword, - IsAdmin: true, - }). - Error - }, +func migrateCreateInitUser() gormigrate.Migration { + return gormigrate.Migration{ + ID: "202002192019", + Migrate: func(tx *gorm.DB) error { + const ( + initUsername = "admin" + initPassword = "admin" + ) + err := tx. + Where("name=?", initUsername). + First(&User{}). + Error + if !gorm.IsRecordNotFoundError(err) { + return nil + } + return tx.Create(&User{ + Name: initUsername, + Password: initPassword, + IsAdmin: true, + }). + Error + }, + } } -var migrationMergePlaylist = gormigrate.Migration{ - ID: "202002192222", - Migrate: func(tx *gorm.DB) error { - if !tx.HasTable("playlist_items") { - return nil - } - return tx.Exec(` +func migrateMergePlaylist() gormigrate.Migration { + return gormigrate.Migration{ + ID: "202002192222", + Migrate: func(tx *gorm.DB) error { + if !tx.HasTable("playlist_items") { + return nil + } + return tx.Exec(` UPDATE playlists SET items=( SELECT group_concat(track_id) FROM ( SELECT track_id @@ -66,78 +69,87 @@ var migrationMergePlaylist = gormigrate.Migration{ ORDER BY created_at ) ); DROP TABLE playlist_items;`, - ). - Error - }, + ). + Error + }, + } } -var migrationCreateTranscode = gormigrate.Migration{ - ID: "202003111222", - Migrate: func(tx *gorm.DB) error { - return tx.AutoMigrate( - TranscodePreference{}, - ). - Error - }, +func migrateCreateTranscode() gormigrate.Migration { + return gormigrate.Migration{ + ID: "202003111222", + Migrate: func(tx *gorm.DB) error { + return tx.AutoMigrate( + TranscodePreference{}, + ). + Error + }, + } } -var migrationAddGenre = gormigrate.Migration{ - ID: "202003121330", - Migrate: func(tx *gorm.DB) error { - return tx.AutoMigrate( - Genre{}, - Album{}, - Track{}, - ). - Error - }, +func migrateAddGenre() gormigrate.Migration { + return gormigrate.Migration{ + ID: "202003121330", + Migrate: func(tx *gorm.DB) error { + return tx.AutoMigrate( + Genre{}, + Album{}, + Track{}, + ). + Error + }, + } } -var migrationUpdateTranscodePrefIDX = gormigrate.Migration{ - ID: "202003241509", - Migrate: func(tx *gorm.DB) error { - var hasIDX int - tx. - Select("1"). - Table("sqlite_master"). - Where("type = ?", "index"). - Where("name = ?", "idx_user_id_client"). - Count(&hasIDX) - if hasIDX == 1 { - // index already exists - return nil - } - step := tx.Exec(` +func migrateUpdateTranscodePrefIDX() gormigrate.Migration { + return gormigrate.Migration{ + ID: "202003241509", + Migrate: func(tx *gorm.DB) error { + var hasIDX int + tx. + Select("1"). + Table("sqlite_master"). + Where("type = ?", "index"). + Where("name = ?", "idx_user_id_client"). + Count(&hasIDX) + if hasIDX == 1 { + // index already exists + return nil + } + step := tx.Exec(` ALTER TABLE transcode_preferences RENAME TO transcode_preferences_orig; `) - if err := step.Error; err != nil { - return fmt.Errorf("step rename: %w", err) - } - step = tx.AutoMigrate( - TranscodePreference{}, - ) - if err := step.Error; err != nil { - return fmt.Errorf("step create: %w", err) - } - step = tx.Exec(` + if err := step.Error; err != nil { + return fmt.Errorf("step rename: %w", err) + } + step = tx.AutoMigrate( + TranscodePreference{}, + ) + if err := step.Error; err != nil { + return fmt.Errorf("step create: %w", err) + } + step = tx.Exec(` INSERT INTO transcode_preferences (user_id, client, profile) SELECT user_id, client, profile FROM transcode_preferences_orig; DROP TABLE transcode_preferences_orig; `) - if err := step.Error; err != nil { - return fmt.Errorf("step copy: %w", err) - } - return nil - }, + if err := step.Error; err != nil { + return fmt.Errorf("step copy: %w", err) + } + return nil + }, + } } -var migrationAddAlbumIDX = gormigrate.Migration{ - ID: "202004302006", - Migrate: func(tx *gorm.DB) error { - return tx.AutoMigrate( - Album{}, - ). - Error - }, +func migrateAddAlbumIDX() gormigrate.Migration { + return gormigrate.Migration{ + ID: "202004302006", + Migrate: func(tx *gorm.DB) error { + return tx.AutoMigrate( + Album{}, + ). + Error + }, + } } diff --git a/server/db/model.go b/server/db/model.go index b613bf3..16db3fe 100644 --- a/server/db/model.go +++ b/server/db/model.go @@ -94,8 +94,8 @@ func (t *Track) Ext() string { } func (t *Track) MIME() string { - ext := t.Ext() - return mime.Types[ext] + v, _ := mime.FromExtension(t.Ext()) + return v } func (t *Track) RelPath() string { diff --git a/server/encode/encode.go b/server/encode/encode.go index bad408e..6c1023d 100644 --- a/server/encode/encode.go +++ b/server/encode/encode.go @@ -13,6 +13,10 @@ import ( "github.com/cespare/xxhash" ) +const ( + buffLen = 4096 +) + type Profile struct { Format string Bitrate int @@ -20,15 +24,14 @@ type Profile struct { forceRG bool } -var ( - Profiles = map[string]*Profile{ +func Profiles() map[string]Profile { + return map[string]Profile{ "mp3": {"mp3", 128, []string{"-c:a", "libmp3lame"}, false}, "mp3_rg": {"mp3", 128, []string{"-c:a", "libmp3lame"}, true}, "opus": {"opus", 96, []string{"-c:a", "libopus", "-vbr", "constrained"}, false}, "opus_rg": {"opus", 96, []string{"-c:a", "libopus", "-vbr", "constrained"}, true}, } - bufLen = 4096 -) +} // copy command output to http response body using io.copy (simpler, but may increase ttfb) //nolint:deadcode,unused @@ -45,7 +48,7 @@ func copyCmdOutput(out, cache io.Writer, pipeReader io.Reader) { // copy command output to http response manually with a buffer (should reduce ttfb) //nolint:deadcode,unused func writeCmdOutput(out, cache io.Writer, pipeReader io.ReadCloser) { - buffer := make([]byte, bufLen) + buffer := make([]byte, buffLen) for { n, err := pipeReader.Read(buffer) if err != nil { @@ -70,7 +73,7 @@ func writeCmdOutput(out, cache io.Writer, pipeReader io.ReadCloser) { } // pre-format the ffmpeg command with needed options -func ffmpegCommand(filePath string, profile *Profile, bitrate string) *exec.Cmd { +func ffmpegCommand(filePath string, profile Profile, bitrate string) *exec.Cmd { args := []string{ "-v", "0", "-i", filePath, @@ -94,7 +97,7 @@ func ffmpegCommand(filePath string, profile *Profile, bitrate string) *exec.Cmd return exec.Command("/usr/bin/ffmpeg", args...) //nolint:gosec } -func Encode(out io.Writer, trackPath, cachePath string, profile *Profile, bitrate string) error { +func Encode(out io.Writer, trackPath, cachePath string, profile Profile, bitrate string) error { // prepare the command and file descriptors cmd := ffmpegCommand(trackPath, profile, bitrate) pipeReader, pipeWriter := io.Pipe() @@ -126,13 +129,13 @@ func Encode(out io.Writer, trackPath, cachePath string, profile *Profile, bitrat // CacheKey generates the filename for the new transcode save func CacheKey(sourcePath string, profile, bitrate string) string { - format := Profiles[profile].Format + format := Profiles()[profile].Format hash := xxhash.Sum64String(sourcePath) return fmt.Sprintf("%x-%s-%s.%s", hash, profile, bitrate, format) } // GetBitrate checks if the client forces bitrate lower than set in profile -func GetBitrate(clientBitrate int, profile *Profile) string { +func GetBitrate(clientBitrate int, profile Profile) string { bitrate := profile.Bitrate if clientBitrate != 0 && clientBitrate < bitrate { bitrate = clientBitrate diff --git a/server/lastfm/lastfm.go b/server/lastfm/lastfm.go index 809f2b5..f80b3b1 100644 --- a/server/lastfm/lastfm.go +++ b/server/lastfm/lastfm.go @@ -10,16 +10,15 @@ import ( "net/url" "sort" "strconv" - "time" "go.senan.xyz/gonic/server/db" ) -var ( +const ( baseURL = "https://ws.audioscrobbler.com/2.0/" - client = &http.Client{ - Timeout: 10 * time.Second, - } +) + +var ( ErrLastFM = errors.New("last.fm error") ) @@ -27,7 +26,7 @@ var ( func getParamSignature(params url.Values, secret string) string { // the parameters must be in order before hashing - paramKeys := make([]string, 0) + paramKeys := make([]string, 0, len(params)) for k := range params { paramKeys = append(paramKeys, k) } @@ -45,7 +44,7 @@ func getParamSignature(params url.Values, secret string) string { func makeRequest(method string, params url.Values) (LastFM, error) { req, _ := http.NewRequest(method, baseURL, nil) req.URL.RawQuery = params.Encode() - resp, err := client.Do(req) + resp, err := http.DefaultClient.Do(req) if err != nil { return LastFM{}, fmt.Errorf("get: %w", err) } diff --git a/server/mime/mime.go b/server/mime/mime.go index 7a5fd4d..63c8f93 100644 --- a/server/mime/mime.go +++ b/server/mime/mime.go @@ -1,11 +1,18 @@ package mime -var Types = map[string]string{ - "mp3": "audio/mpeg", - "flac": "audio/x-flac", - "aac": "audio/x-aac", - "m4a": "audio/m4a", - "m4b": "audio/m4b", - "ogg": "audio/ogg", - "opus": "audio/ogg", +// this package is at such a high level in the hierarchy because +// it's used by both `server/db` and `server/scanner` + +func FromExtension(ext string) (string, bool) { + types := map[string]string{ + "mp3": "audio/mpeg", + "flac": "audio/x-flac", + "aac": "audio/x-aac", + "m4a": "audio/m4a", + "m4b": "audio/m4b", + "ogg": "audio/ogg", + "opus": "audio/ogg", + } + v, ok := types[ext] + return v, ok } diff --git a/server/scanner/scanner.go b/server/scanner/scanner.go index cfc1919..2ae5536 100644 --- a/server/scanner/scanner.go +++ b/server/scanner/scanner.go @@ -44,7 +44,7 @@ func decoded(in string) string { // isScanning acts as an atomic boolean semaphore. we don't // want to have more than one scan going on at a time -var isScanning int32 +var isScanning int32 //nolint:gochecknoglobals func IsScanning() bool { return atomic.LoadInt32(&isScanning) == 1 @@ -224,19 +224,23 @@ type item struct { stat os.FileInfo } -var coverFilenames = map[string]struct{}{ - "cover.png": {}, - "cover.jpg": {}, - "cover.jpeg": {}, - "folder.png": {}, - "folder.jpg": {}, - "folder.jpeg": {}, - "album.png": {}, - "album.jpg": {}, - "album.jpeg": {}, - "front.png": {}, - "front.jpg": {}, - "front.jpeg": {}, +func isCover(filename string) bool { + known := map[string]struct{}{ + "cover.png": {}, + "cover.jpg": {}, + "cover.jpeg": {}, + "folder.png": {}, + "folder.jpg": {}, + "folder.jpeg": {}, + "album.png": {}, + "album.jpg": {}, + "album.jpeg": {}, + "front.png": {}, + "front.jpg": {}, + "front.jpeg": {}, + } + _, ok := known[filename] + return ok } // ## begin callbacks @@ -267,8 +271,8 @@ func (s *Scanner) callbackItem(fullPath string, info *godirwalk.Dirent) error { if isDir { return s.handleFolder(it) } - lowerFilename := strings.ToLower(filename) - if _, ok := coverFilenames[lowerFilename]; ok { + filenameLow := strings.ToLower(filename) + if isCover(filenameLow) { s.curCover = filename return nil } @@ -276,7 +280,7 @@ func (s *Scanner) callbackItem(fullPath string, info *godirwalk.Dirent) error { if ext == "" { return nil } - if _, ok := mime.Types[ext[1:]]; ok { + if _, ok := mime.FromExtension(ext[1:]); ok { return s.handleTrack(it) } return nil