remove globals

This commit is contained in:
sentriz
2020-05-03 04:43:00 +01:00
parent 9bf80f4b18
commit ee9335f71e
11 changed files with 188 additions and 163 deletions

View File

@@ -236,6 +236,7 @@ type Flash struct {
Type FlashType Type FlashType
} }
//nolint:gochecknoinits
func init() { func init() {
gob.Register(&Flash{}) gob.Register(&Flash{})
} }

View File

@@ -82,7 +82,7 @@ func (c *Controller) ServeHome(r *http.Request) *Response {
c.DB. c.DB.
Where("user_id=?", user.ID). Where("user_id=?", user.ID).
Find(&data.TranscodePreferences) Find(&data.TranscodePreferences)
for profile := range encode.Profiles { for profile := range encode.Profiles() {
data.TranscodeProfiles = append(data.TranscodeProfiles, profile) data.TranscodeProfiles = append(data.TranscodeProfiles, profile)
} }
// //

View File

@@ -72,7 +72,7 @@ func serveTrackRaw(w http.ResponseWriter, r *http.Request, opts serveTrackOption
} }
func serveTrackEncode(w http.ResponseWriter, r *http.Request, opts serveTrackOptions) { 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) bitrate := encode.GetBitrate(opts.maxBitrate, profile)
trackPath := path.Join(opts.musicPath, opts.track.RelPath()) trackPath := path.Join(opts.musicPath, opts.track.RelPath())
cacheKey := encode.CacheKey(trackPath, opts.pref.Profile, bitrate) cacheKey := encode.CacheKey(trackPath, opts.pref.Profile, bitrate)

View File

@@ -7,7 +7,7 @@ import (
"go.senan.xyz/gonic/version" "go.senan.xyz/gonic/version"
) )
var ( const (
apiVersion = "1.9.0" apiVersion = "1.9.0"
xmlns = "http://subsonic.org/restapi" xmlns = "http://subsonic.org/restapi"
) )

View File

@@ -31,9 +31,8 @@ func wrapMigrations(migrs ...gormigrate.Migration) []*gormigrate.Migration {
return ret return ret
} }
var ( func defaultOptions() url.Values {
dbMaxOpenConns = 1 return url.Values{
dbOptions = url.Values{
// with this, multiple connections share a single data and schema cache. // with this, multiple connections share a single data and schema cache.
// see https://www.sqlite.org/sharedcache.html // see https://www.sqlite.org/sharedcache.html
"cache": {"shared"}, "cache": {"shared"},
@@ -43,7 +42,7 @@ var (
"_journal_mode": {"WAL"}, "_journal_mode": {"WAL"},
"_foreign_keys": {"true"}, "_foreign_keys": {"true"},
} }
) }
type DB struct { type DB struct {
*gorm.DB *gorm.DB
@@ -55,13 +54,13 @@ func New(path string) (*DB, error) {
Scheme: "file", Scheme: "file",
Opaque: path, Opaque: path,
} }
url.RawQuery = dbOptions.Encode() url.RawQuery = defaultOptions().Encode()
db, err := gorm.Open("sqlite3", url.String()) db, err := gorm.Open("sqlite3", url.String())
if err != nil { if err != nil {
return nil, fmt.Errorf("with gorm: %w", err) return nil, fmt.Errorf("with gorm: %w", err)
} }
db.SetLogger(log.New(os.Stdout, "gorm ", 0)) db.SetLogger(log.New(os.Stdout, "gorm ", 0))
db.DB().SetMaxOpenConns(dbMaxOpenConns) db.DB().SetMaxOpenConns(1)
migrOptions := &gormigrate.Options{ migrOptions := &gormigrate.Options{
TableName: "migrations", TableName: "migrations",
IDColumnName: "id", IDColumnName: "id",
@@ -69,13 +68,13 @@ func New(path string) (*DB, error) {
UseTransaction: false, UseTransaction: false,
} }
migr := gormigrate.New(db, migrOptions, wrapMigrations( migr := gormigrate.New(db, migrOptions, wrapMigrations(
migrationInitSchema, migrateInitSchema(),
migrationCreateInitUser, migrateCreateInitUser(),
migrationMergePlaylist, migrateMergePlaylist(),
migrationCreateTranscode, migrateCreateTranscode(),
migrationAddGenre, migrateAddGenre(),
migrationUpdateTranscodePrefIDX, migrateUpdateTranscodePrefIDX(),
migrationAddAlbumIDX, migrateAddAlbumIDX(),
)) ))
if err = migr.Migrate(); err != nil { if err = migr.Migrate(); err != nil {
return nil, fmt.Errorf("migrating to latest version: %w", err) return nil, fmt.Errorf("migrating to latest version: %w", err)

View File

@@ -9,55 +9,58 @@ import (
// $ date '+%Y%m%d%H%M' // $ date '+%Y%m%d%H%M'
// not really a migration func migrateInitSchema() gormigrate.Migration {
var migrationInitSchema = gormigrate.Migration{ return gormigrate.Migration{
ID: "202002192100", ID: "202002192100",
Migrate: func(tx *gorm.DB) error { Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate( return tx.AutoMigrate(
Artist{}, Artist{},
Track{}, Track{},
User{}, User{},
Setting{}, Setting{},
Play{}, Play{},
Album{}, Album{},
Playlist{}, Playlist{},
PlayQueue{}, PlayQueue{},
). ).
Error Error
}, },
}
} }
// not really a migration func migrateCreateInitUser() gormigrate.Migration {
var migrationCreateInitUser = gormigrate.Migration{ return gormigrate.Migration{
ID: "202002192019", ID: "202002192019",
Migrate: func(tx *gorm.DB) error { Migrate: func(tx *gorm.DB) error {
const ( const (
initUsername = "admin" initUsername = "admin"
initPassword = "admin" initPassword = "admin"
) )
err := tx. err := tx.
Where("name=?", initUsername). Where("name=?", initUsername).
First(&User{}). First(&User{}).
Error Error
if !gorm.IsRecordNotFoundError(err) { if !gorm.IsRecordNotFoundError(err) {
return nil return nil
} }
return tx.Create(&User{ return tx.Create(&User{
Name: initUsername, Name: initUsername,
Password: initPassword, Password: initPassword,
IsAdmin: true, IsAdmin: true,
}). }).
Error Error
}, },
}
} }
var migrationMergePlaylist = gormigrate.Migration{ func migrateMergePlaylist() gormigrate.Migration {
ID: "202002192222", return gormigrate.Migration{
Migrate: func(tx *gorm.DB) error { ID: "202002192222",
if !tx.HasTable("playlist_items") { Migrate: func(tx *gorm.DB) error {
return nil if !tx.HasTable("playlist_items") {
} return nil
return tx.Exec(` }
return tx.Exec(`
UPDATE playlists UPDATE playlists
SET items=( SELECT group_concat(track_id) FROM ( SET items=( SELECT group_concat(track_id) FROM (
SELECT track_id SELECT track_id
@@ -66,78 +69,87 @@ var migrationMergePlaylist = gormigrate.Migration{
ORDER BY created_at ORDER BY created_at
) ); ) );
DROP TABLE playlist_items;`, DROP TABLE playlist_items;`,
). ).
Error Error
}, },
}
} }
var migrationCreateTranscode = gormigrate.Migration{ func migrateCreateTranscode() gormigrate.Migration {
ID: "202003111222", return gormigrate.Migration{
Migrate: func(tx *gorm.DB) error { ID: "202003111222",
return tx.AutoMigrate( Migrate: func(tx *gorm.DB) error {
TranscodePreference{}, return tx.AutoMigrate(
). TranscodePreference{},
Error ).
}, Error
},
}
} }
var migrationAddGenre = gormigrate.Migration{ func migrateAddGenre() gormigrate.Migration {
ID: "202003121330", return gormigrate.Migration{
Migrate: func(tx *gorm.DB) error { ID: "202003121330",
return tx.AutoMigrate( Migrate: func(tx *gorm.DB) error {
Genre{}, return tx.AutoMigrate(
Album{}, Genre{},
Track{}, Album{},
). Track{},
Error ).
}, Error
},
}
} }
var migrationUpdateTranscodePrefIDX = gormigrate.Migration{ func migrateUpdateTranscodePrefIDX() gormigrate.Migration {
ID: "202003241509", return gormigrate.Migration{
Migrate: func(tx *gorm.DB) error { ID: "202003241509",
var hasIDX int Migrate: func(tx *gorm.DB) error {
tx. var hasIDX int
Select("1"). tx.
Table("sqlite_master"). Select("1").
Where("type = ?", "index"). Table("sqlite_master").
Where("name = ?", "idx_user_id_client"). Where("type = ?", "index").
Count(&hasIDX) Where("name = ?", "idx_user_id_client").
if hasIDX == 1 { Count(&hasIDX)
// index already exists if hasIDX == 1 {
return nil // index already exists
} return nil
step := tx.Exec(` }
step := tx.Exec(`
ALTER TABLE transcode_preferences RENAME TO transcode_preferences_orig; ALTER TABLE transcode_preferences RENAME TO transcode_preferences_orig;
`) `)
if err := step.Error; err != nil { if err := step.Error; err != nil {
return fmt.Errorf("step rename: %w", err) return fmt.Errorf("step rename: %w", err)
} }
step = tx.AutoMigrate( step = tx.AutoMigrate(
TranscodePreference{}, TranscodePreference{},
) )
if err := step.Error; err != nil { if err := step.Error; err != nil {
return fmt.Errorf("step create: %w", err) return fmt.Errorf("step create: %w", err)
} }
step = tx.Exec(` step = tx.Exec(`
INSERT INTO transcode_preferences (user_id, client, profile) INSERT INTO transcode_preferences (user_id, client, profile)
SELECT user_id, client, profile SELECT user_id, client, profile
FROM transcode_preferences_orig; FROM transcode_preferences_orig;
DROP TABLE transcode_preferences_orig; DROP TABLE transcode_preferences_orig;
`) `)
if err := step.Error; err != nil { if err := step.Error; err != nil {
return fmt.Errorf("step copy: %w", err) return fmt.Errorf("step copy: %w", err)
} }
return nil return nil
}, },
}
} }
var migrationAddAlbumIDX = gormigrate.Migration{ func migrateAddAlbumIDX() gormigrate.Migration {
ID: "202004302006", return gormigrate.Migration{
Migrate: func(tx *gorm.DB) error { ID: "202004302006",
return tx.AutoMigrate( Migrate: func(tx *gorm.DB) error {
Album{}, return tx.AutoMigrate(
). Album{},
Error ).
}, Error
},
}
} }

View File

@@ -94,8 +94,8 @@ func (t *Track) Ext() string {
} }
func (t *Track) MIME() string { func (t *Track) MIME() string {
ext := t.Ext() v, _ := mime.FromExtension(t.Ext())
return mime.Types[ext] return v
} }
func (t *Track) RelPath() string { func (t *Track) RelPath() string {

View File

@@ -13,6 +13,10 @@ import (
"github.com/cespare/xxhash" "github.com/cespare/xxhash"
) )
const (
buffLen = 4096
)
type Profile struct { type Profile struct {
Format string Format string
Bitrate int Bitrate int
@@ -20,15 +24,14 @@ type Profile struct {
forceRG bool forceRG bool
} }
var ( func Profiles() map[string]Profile {
Profiles = map[string]*Profile{ return map[string]Profile{
"mp3": {"mp3", 128, []string{"-c:a", "libmp3lame"}, false}, "mp3": {"mp3", 128, []string{"-c:a", "libmp3lame"}, false},
"mp3_rg": {"mp3", 128, []string{"-c:a", "libmp3lame"}, true}, "mp3_rg": {"mp3", 128, []string{"-c:a", "libmp3lame"}, true},
"opus": {"opus", 96, []string{"-c:a", "libopus", "-vbr", "constrained"}, false}, "opus": {"opus", 96, []string{"-c:a", "libopus", "-vbr", "constrained"}, false},
"opus_rg": {"opus", 96, []string{"-c:a", "libopus", "-vbr", "constrained"}, true}, "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) // copy command output to http response body using io.copy (simpler, but may increase ttfb)
//nolint:deadcode,unused //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) // copy command output to http response manually with a buffer (should reduce ttfb)
//nolint:deadcode,unused //nolint:deadcode,unused
func writeCmdOutput(out, cache io.Writer, pipeReader io.ReadCloser) { func writeCmdOutput(out, cache io.Writer, pipeReader io.ReadCloser) {
buffer := make([]byte, bufLen) buffer := make([]byte, buffLen)
for { for {
n, err := pipeReader.Read(buffer) n, err := pipeReader.Read(buffer)
if err != nil { if err != nil {
@@ -70,7 +73,7 @@ func writeCmdOutput(out, cache io.Writer, pipeReader io.ReadCloser) {
} }
// pre-format the ffmpeg command with needed options // 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{ args := []string{
"-v", "0", "-v", "0",
"-i", filePath, "-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 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 // prepare the command and file descriptors
cmd := ffmpegCommand(trackPath, profile, bitrate) cmd := ffmpegCommand(trackPath, profile, bitrate)
pipeReader, pipeWriter := io.Pipe() 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 // CacheKey generates the filename for the new transcode save
func CacheKey(sourcePath string, profile, bitrate string) string { func CacheKey(sourcePath string, profile, bitrate string) string {
format := Profiles[profile].Format format := Profiles()[profile].Format
hash := xxhash.Sum64String(sourcePath) hash := xxhash.Sum64String(sourcePath)
return fmt.Sprintf("%x-%s-%s.%s", hash, profile, bitrate, format) return fmt.Sprintf("%x-%s-%s.%s", hash, profile, bitrate, format)
} }
// GetBitrate checks if the client forces bitrate lower than set in profile // 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 bitrate := profile.Bitrate
if clientBitrate != 0 && clientBitrate < bitrate { if clientBitrate != 0 && clientBitrate < bitrate {
bitrate = clientBitrate bitrate = clientBitrate

View File

@@ -10,16 +10,15 @@ import (
"net/url" "net/url"
"sort" "sort"
"strconv" "strconv"
"time"
"go.senan.xyz/gonic/server/db" "go.senan.xyz/gonic/server/db"
) )
var ( const (
baseURL = "https://ws.audioscrobbler.com/2.0/" baseURL = "https://ws.audioscrobbler.com/2.0/"
client = &http.Client{ )
Timeout: 10 * time.Second,
} var (
ErrLastFM = errors.New("last.fm error") ErrLastFM = errors.New("last.fm error")
) )
@@ -27,7 +26,7 @@ var (
func getParamSignature(params url.Values, secret string) string { func getParamSignature(params url.Values, secret string) string {
// the parameters must be in order before hashing // the parameters must be in order before hashing
paramKeys := make([]string, 0) paramKeys := make([]string, 0, len(params))
for k := range params { for k := range params {
paramKeys = append(paramKeys, k) 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) { func makeRequest(method string, params url.Values) (LastFM, error) {
req, _ := http.NewRequest(method, baseURL, nil) req, _ := http.NewRequest(method, baseURL, nil)
req.URL.RawQuery = params.Encode() req.URL.RawQuery = params.Encode()
resp, err := client.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return LastFM{}, fmt.Errorf("get: %w", err) return LastFM{}, fmt.Errorf("get: %w", err)
} }

View File

@@ -1,11 +1,18 @@
package mime package mime
var Types = map[string]string{ // this package is at such a high level in the hierarchy because
"mp3": "audio/mpeg", // it's used by both `server/db` and `server/scanner`
"flac": "audio/x-flac",
"aac": "audio/x-aac", func FromExtension(ext string) (string, bool) {
"m4a": "audio/m4a", types := map[string]string{
"m4b": "audio/m4b", "mp3": "audio/mpeg",
"ogg": "audio/ogg", "flac": "audio/x-flac",
"opus": "audio/ogg", "aac": "audio/x-aac",
"m4a": "audio/m4a",
"m4b": "audio/m4b",
"ogg": "audio/ogg",
"opus": "audio/ogg",
}
v, ok := types[ext]
return v, ok
} }

View File

@@ -44,7 +44,7 @@ func decoded(in string) string {
// isScanning acts as an atomic boolean semaphore. we don't // isScanning acts as an atomic boolean semaphore. we don't
// want to have more than one scan going on at a time // want to have more than one scan going on at a time
var isScanning int32 var isScanning int32 //nolint:gochecknoglobals
func IsScanning() bool { func IsScanning() bool {
return atomic.LoadInt32(&isScanning) == 1 return atomic.LoadInt32(&isScanning) == 1
@@ -224,19 +224,23 @@ type item struct {
stat os.FileInfo stat os.FileInfo
} }
var coverFilenames = map[string]struct{}{ func isCover(filename string) bool {
"cover.png": {}, known := map[string]struct{}{
"cover.jpg": {}, "cover.png": {},
"cover.jpeg": {}, "cover.jpg": {},
"folder.png": {}, "cover.jpeg": {},
"folder.jpg": {}, "folder.png": {},
"folder.jpeg": {}, "folder.jpg": {},
"album.png": {}, "folder.jpeg": {},
"album.jpg": {}, "album.png": {},
"album.jpeg": {}, "album.jpg": {},
"front.png": {}, "album.jpeg": {},
"front.jpg": {}, "front.png": {},
"front.jpeg": {}, "front.jpg": {},
"front.jpeg": {},
}
_, ok := known[filename]
return ok
} }
// ## begin callbacks // ## begin callbacks
@@ -267,8 +271,8 @@ func (s *Scanner) callbackItem(fullPath string, info *godirwalk.Dirent) error {
if isDir { if isDir {
return s.handleFolder(it) return s.handleFolder(it)
} }
lowerFilename := strings.ToLower(filename) filenameLow := strings.ToLower(filename)
if _, ok := coverFilenames[lowerFilename]; ok { if isCover(filenameLow) {
s.curCover = filename s.curCover = filename
return nil return nil
} }
@@ -276,7 +280,7 @@ func (s *Scanner) callbackItem(fullPath string, info *godirwalk.Dirent) error {
if ext == "" { if ext == "" {
return nil return nil
} }
if _, ok := mime.Types[ext[1:]]; ok { if _, ok := mime.FromExtension(ext[1:]); ok {
return s.handleTrack(it) return s.handleTrack(it)
} }
return nil return nil