Compare commits

...

8 commits
v0.1.5 ... main

Author SHA1 Message Date
Gabe Farrell
0ec7b458cc
ui: tweaks and fixes (#194)
* reduce min width of top chart on mobile

* adjust error page style

* adjust h1 line height
2026-02-04 13:41:12 -05:00
Gabe Farrell
531c72899c
fix: add null check for top charts bg gradient (#193) 2026-02-03 11:23:30 -05:00
Gabe Farrell
b06685c1af
fix: rewind navigation (#191) 2026-02-02 15:06:13 -05:00
Gabe Farrell
64236c99c9
fix: invalid json response when login gate is disabled (#184) 2026-01-26 14:49:30 -05:00
Gabe Farrell
42b32c7920
feat: add api key auth to web api (#183) 2026-01-26 13:48:43 -05:00
PythonGermany
bf1c03e9fd
docs: fix typo in index.mdx (#182) 2026-01-26 13:43:01 -05:00
Gabe Farrell
35e104c97e
fix: gradient background on top charts (#181) 2026-01-26 13:03:27 -05:00
Gabe Farrell
c8a11ef018
fix: ensure mbids in mbidmapping are discovered (#180) 2026-01-25 15:51:07 -05:00
18 changed files with 509 additions and 359 deletions

View file

@ -58,6 +58,7 @@
--header-sm: 16px; --header-sm: 16px;
--header-xl-weight: 600; --header-xl-weight: 600;
--header-weight: 600; --header-weight: 600;
--header-line-height: 3rem;
} }
@media (min-width: 60rem) { @media (min-width: 60rem) {
@ -68,6 +69,7 @@
--header-sm: 16px; --header-sm: 16px;
--header-xl-weight: 600; --header-xl-weight: 600;
--header-weight: 600; --header-weight: 600;
--header-line-height: 1.3em;
} }
} }
@ -98,6 +100,7 @@ h1 {
font-family: "League Spartan"; font-family: "League Spartan";
font-weight: var(--header-weight); font-weight: var(--header-weight);
font-size: var(--header-xl); font-size: var(--header-xl);
line-height: var(--header-line-height);
} }
h2 { h2 {
font-family: "League Spartan"; font-family: "League Spartan";

View file

@ -116,12 +116,12 @@ export function ErrorBoundary() {
<AppProvider> <AppProvider>
<ThemeProvider> <ThemeProvider>
<title>{title}</title> <title>{title}</title>
<Sidebar />
<div className="flex"> <div className="flex">
<Sidebar />
<div className="w-full flex flex-col"> <div className="w-full flex flex-col">
<main className="pt-16 p-4 container mx-auto flex-grow"> <main className="pt-16 p-4 mx-auto flex-grow">
<div className="flex gap-4 items-end"> <div className="md:flex gap-4">
<img className="w-[200px] rounded" src="../yuu.jpg" /> <img className="w-[200px] rounded mb-3" src="../yuu.jpg" />
<div> <div>
<h1>{message}</h1> <h1>{message}</h1>
<p>{details}</p> <p>{details}</p>

View file

@ -30,7 +30,7 @@ export default function AlbumChart() {
initialData={initialData} initialData={initialData}
endpoint="chart/top-albums" endpoint="chart/top-albums"
render={({ data, page, onNext, onPrev }) => ( render={({ data, page, onNext, onPrev }) => (
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5 w-full">
<div className="flex gap-15 mx-auto"> <div className="flex gap-15 mx-auto">
<button className="default" onClick={onPrev} disabled={page <= 1}> <button className="default" onClick={onPrev} disabled={page <= 1}>
Prev Prev
@ -47,7 +47,7 @@ export default function AlbumChart() {
ranked ranked
separators separators
data={data} data={data}
className="w-[400px] sm:w-[600px]" className="w-11/12 sm:w-[600px]"
type="album" type="album"
/> />
<div className="flex gap-15 mx-auto"> <div className="flex gap-15 mx-auto">

View file

@ -30,7 +30,7 @@ export default function Artist() {
initialData={initialData} initialData={initialData}
endpoint="chart/top-artists" endpoint="chart/top-artists"
render={({ data, page, onNext, onPrev }) => ( render={({ data, page, onNext, onPrev }) => (
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5 w-full">
<div className="flex gap-15 mx-auto"> <div className="flex gap-15 mx-auto">
<button className="default" onClick={onPrev} disabled={page <= 1}> <button className="default" onClick={onPrev} disabled={page <= 1}>
Prev Prev
@ -47,7 +47,7 @@ export default function Artist() {
ranked ranked
separators separators
data={data} data={data}
className="w-[400px] sm:w-[600px]" className="w-11/12 sm:w-[600px]"
type="artist" type="artist"
/> />
<div className="flex gap-15 mx-auto"> <div className="flex gap-15 mx-auto">

View file

@ -40,7 +40,7 @@ export default function ChartLayout<T>({
useEffect(() => { useEffect(() => {
if ((data?.items?.length ?? 0) === 0) return; if ((data?.items?.length ?? 0) === 0) return;
const img = (data.items[0] as any)?.image; const img = (data.items[0] as any)?.item?.image;
if (!img) return; if (!img) return;
average(imageUrl(img, "small"), { amount: 1 }).then((color) => { average(imageUrl(img, "small"), { amount: 1 }).then((color) => {

View file

@ -30,7 +30,7 @@ export default function TrackChart() {
initialData={initialData} initialData={initialData}
endpoint="chart/top-tracks" endpoint="chart/top-tracks"
render={({ data, page, onNext, onPrev }) => ( render={({ data, page, onNext, onPrev }) => (
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5 w-full">
<div className="flex gap-15 mx-auto"> <div className="flex gap-15 mx-auto">
<button className="default" onClick={onPrev} disabled={page <= 1}> <button className="default" onClick={onPrev} disabled={page <= 1}>
Prev Prev
@ -47,7 +47,7 @@ export default function TrackChart() {
ranked ranked
separators separators
data={data} data={data}
className="w-[400px] sm:w-[600px]" className="w-11/12 sm:w-[600px]"
type="track" type="track"
/> />
<div className="flex gap-15 mx-auto"> <div className="flex gap-15 mx-auto">

View file

@ -29,10 +29,12 @@ const months = [
export async function clientLoader({ request }: LoaderFunctionArgs) { export async function clientLoader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url); const url = new URL(request.url);
const year = const year = parseInt(
parseInt(url.searchParams.get("year") || "0") || getRewindParams().year; url.searchParams.get("year") || getRewindParams().year.toString()
const month = );
parseInt(url.searchParams.get("month") || "0") || getRewindParams().month; const month = parseInt(
url.searchParams.get("month") || getRewindParams().month.toString()
);
const res = await fetch(`/apis/web/v1/summary?year=${year}&month=${month}`); const res = await fetch(`/apis/web/v1/summary?year=${year}&month=${month}`);
if (!res.ok) { if (!res.ok) {
@ -46,10 +48,12 @@ export async function clientLoader({ request }: LoaderFunctionArgs) {
export default function RewindPage() { export default function RewindPage() {
const currentParams = new URLSearchParams(location.search); const currentParams = new URLSearchParams(location.search);
let year = let year = parseInt(
parseInt(currentParams.get("year") || "0") || getRewindParams().year; currentParams.get("year") || getRewindParams().year.toString()
let month = );
parseInt(currentParams.get("month") || "0") || getRewindParams().month; let month = parseInt(
currentParams.get("month") || getRewindParams().month.toString()
);
const navigate = useNavigate(); const navigate = useNavigate();
const [showTime, setShowTime] = useState(false); const [showTime, setShowTime] = useState(false);
const { stats: stats } = useLoaderData<{ stats: RewindStats }>(); const { stats: stats } = useLoaderData<{ stats: RewindStats }>();
@ -73,10 +77,8 @@ export default function RewindPage() {
for (const key in params) { for (const key in params) {
const val = params[key]; const val = params[key];
if (val !== null && val !== "0") { if (val !== null) {
nextParams.set(key, val); nextParams.set(key, val);
} else {
nextParams.delete(key);
} }
} }
@ -99,6 +101,7 @@ export default function RewindPage() {
month -= 1; month -= 1;
} }
} }
console.log(`Month: ${month}`);
updateParams({ updateParams({
year: year.toString(), year: year.toString(),
@ -154,7 +157,12 @@ export default function RewindPage() {
<button <button
onClick={() => navigateMonth("next")} onClick={() => navigateMonth("next")}
className="p-2 disabled:text-(--color-fg-tertiary)" className="p-2 disabled:text-(--color-fg-tertiary)"
disabled={new Date(year, month) > new Date()} disabled={
// next month is current or future month and
month >= new Date().getMonth() &&
// we are looking at current (or future) year
year >= new Date().getFullYear()
}
> >
<ChevronRight size={20} /> <ChevronRight size={20} />
</button> </button>

View file

@ -28,7 +28,7 @@ import { Card, CardGrid } from '@astrojs/starlight/components';
Koito can be connected to any music server or client that allows for custom ListenBrainz URLs. Koito can be connected to any music server or client that allows for custom ListenBrainz URLs.
</Card> </Card>
<Card title="Scrobbler relay" icon="rocket"> <Card title="Scrobbler relay" icon="rocket">
Automatically relay listens submitted to your Koito instance to other ListenBrainz compatble servers. Automatically relay listens submitted to your Koito instance to other ListenBrainz compatible servers.
</Card> </Card>
<Card title="Automatic data fetching" icon="download"> <Card title="Automatic data fetching" icon="download">
Koito automatically fetches data from MusicBrainz and images from Deezer and Cover Art Archive to compliment what is provided by your music server. Koito automatically fetches data from MusicBrainz and images from Deezer and Cover Art Archive to compliment what is provided by your music server.

View file

@ -264,6 +264,34 @@ func TestImportListenBrainz_MbzDisabled(t *testing.T) {
truncateTestData(t) truncateTestData(t)
} }
func TestImportListenBrainz_MBIDMapping(t *testing.T) {
src := path.Join("..", "test_assets", "listenbrainz_shoko1_123456789.zip")
destDir := filepath.Join(cfg.ConfigDir(), "import")
dest := filepath.Join(destDir, "listenbrainz_shoko1_123456789.zip")
// not going to make the dest dir because engine should make it already
input, err := os.ReadFile(src)
require.NoError(t, err)
require.NoError(t, os.WriteFile(dest, input, os.ModePerm))
engine.RunImporter(logger.Get(), store, &mbz.MbzErrorCaller{})
album, err := store.GetAlbum(context.Background(), db.GetAlbumOpts{MusicBrainzID: uuid.MustParse("177ebc28-0115-3897-8eb3-ebf74ce23790")})
require.NoError(t, err)
assert.Equal(t, "Zombie", album.Title)
artist, err := store.GetArtist(context.Background(), db.GetArtistOpts{MusicBrainzID: uuid.MustParse("c98d40fd-f6cf-4b26-883e-eaa515ee2851")})
require.NoError(t, err)
assert.Equal(t, "The Cranberries", artist.Name)
track, err := store.GetTrack(context.Background(), db.GetTrackOpts{MusicBrainzID: uuid.MustParse("3bbeb4e3-ab6d-460d-bfc5-de49e4251061")})
require.NoError(t, err)
assert.Equal(t, "Zombie", track.Title)
truncateTestData(t)
}
func TestImportKoito(t *testing.T) { func TestImportKoito(t *testing.T) {
src := path.Join("..", "test_assets", "koito_export_test.json") src := path.Join("..", "test_assets", "koito_export_test.json")

View file

@ -356,6 +356,51 @@ func TestDelete(t *testing.T) {
truncateTestData(t) truncateTestData(t)
} }
func TestLoginGate(t *testing.T) {
t.Run("Submit Listens", doSubmitListens)
req, err := http.NewRequest("DELETE", host()+"/apis/web/v1/artist?id=1", nil)
require.NoError(t, err)
req.Header.Add("Authorization", "Token "+apikey)
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.Equal(t, 204, resp.StatusCode)
req, err = http.NewRequest("GET", host()+"/apis/web/v1/artist?id=3", nil)
require.NoError(t, err)
resp, err = http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
var artist models.Artist
err = json.NewDecoder(resp.Body).Decode(&artist)
require.NoError(t, err)
assert.Equal(t, "ネクライトーキー", artist.Name)
cfg.SetLoginGate(true)
req, err = http.NewRequest("GET", host()+"/apis/web/v1/artist?id=3", nil)
require.NoError(t, err)
// req.Header.Add("Authorization", "Token "+apikey)
resp, err = http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
req, err = http.NewRequest("GET", host()+"/apis/web/v1/artist?id=3", nil)
require.NoError(t, err)
req.Header.Add("Authorization", "Token "+apikey)
resp, err = http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
err = json.NewDecoder(resp.Body).Decode(&artist)
require.NoError(t, err)
assert.Equal(t, "ネクライトーキー", artist.Name)
cfg.SetLoginGate(false)
truncateTestData(t)
}
func TestAliasesAndSearch(t *testing.T) { func TestAliasesAndSearch(t *testing.T) {
t.Run("Submit Listens", doSubmitListens) t.Run("Submit Listens", doSubmitListens)

View file

@ -0,0 +1,166 @@
package middleware
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/gabehf/koito/internal/cfg"
"github.com/gabehf/koito/internal/db"
"github.com/gabehf/koito/internal/logger"
"github.com/gabehf/koito/internal/models"
"github.com/gabehf/koito/internal/utils"
"github.com/google/uuid"
)
type MiddlwareContextKey string
const (
UserContextKey MiddlwareContextKey = "user"
apikeyContextKey MiddlwareContextKey = "apikeyID"
)
type AuthMode int
const (
AuthModeSessionCookie AuthMode = iota
AuthModeAPIKey
AuthModeSessionOrAPIKey
AuthModeLoginGate
)
func Authenticate(store db.DB, mode AuthMode) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
l := logger.FromContext(ctx)
var user *models.User
var err error
switch mode {
case AuthModeSessionCookie:
user, err = validateSession(ctx, store, r)
case AuthModeAPIKey:
user, err = validateAPIKey(ctx, store, r)
case AuthModeSessionOrAPIKey:
user, err = validateSession(ctx, store, r)
if err != nil || user == nil {
user, err = validateAPIKey(ctx, store, r)
}
case AuthModeLoginGate:
if cfg.LoginGate() {
user, err = validateSession(ctx, store, r)
if err != nil || user == nil {
user, err = validateAPIKey(ctx, store, r)
}
} else {
next.ServeHTTP(w, r)
return
}
}
if err != nil {
l.Err(err).Msg("authentication failed")
utils.WriteError(w, "unauthorized", http.StatusUnauthorized)
return
}
if user == nil {
utils.WriteError(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx = context.WithValue(ctx, UserContextKey, user)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}
func validateSession(ctx context.Context, store db.DB, r *http.Request) (*models.User, error) {
l := logger.FromContext(r.Context())
l.Debug().Msgf("ValidateSession: Checking user authentication via session cookie")
cookie, err := r.Cookie("koito_session")
var sid uuid.UUID
if err == nil {
sid, err = uuid.Parse(cookie.Value)
if err != nil {
l.Err(err).Msg("ValidateSession: Could not parse UUID from session cookie")
return nil, errors.New("session cookie is invalid")
}
} else {
l.Debug().Msgf("ValidateSession: No session cookie found; attempting API key authentication")
return nil, errors.New("session cookie is missing")
}
l.Debug().Msg("ValidateSession: Retrieved login cookie from request")
u, err := store.GetUserBySession(r.Context(), sid)
if err != nil {
l.Err(fmt.Errorf("ValidateSession: %w", err)).Msg("Error accessing database")
return nil, errors.New("internal server error")
}
if u == nil {
l.Debug().Msg("ValidateSession: No user with session id found")
return nil, errors.New("no user with session id found")
}
ctx = context.WithValue(r.Context(), UserContextKey, u)
r = r.WithContext(ctx)
l.Debug().Msgf("ValidateSession: Refreshing session for user '%s'", u.Username)
store.RefreshSession(r.Context(), sid, time.Now().Add(30*24*time.Hour))
l.Debug().Msgf("ValidateSession: Refreshed session for user '%s'", u.Username)
return u, nil
}
func validateAPIKey(ctx context.Context, store db.DB, r *http.Request) (*models.User, error) {
l := logger.FromContext(ctx)
l.Debug().Msg("ValidateApiKey: Checking if user is already authenticated")
authH := r.Header.Get("Authorization")
var token string
if strings.HasPrefix(strings.ToLower(authH), "token ") {
token = strings.TrimSpace(authH[6:]) // strip "Token "
} else {
l.Error().Msg("ValidateApiKey: Authorization header must be formatted 'Token {token}'")
return nil, errors.New("authorization header is invalid")
}
u, err := store.GetUserByApiKey(ctx, token)
if err != nil {
l.Err(err).Msg("ValidateApiKey: Failed to get user from database using api key")
return nil, errors.New("internal server error")
}
if u == nil {
l.Debug().Msg("ValidateApiKey: API key does not exist")
return nil, errors.New("authorization token is invalid")
}
ctx = context.WithValue(r.Context(), UserContextKey, u)
r = r.WithContext(ctx)
return u, nil
}
func GetUserFromContext(ctx context.Context) *models.User {
user, ok := ctx.Value(UserContextKey).(*models.User)
if !ok {
return nil
}
return user
}

View file

@ -1,125 +0,0 @@
package middleware
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/gabehf/koito/internal/db"
"github.com/gabehf/koito/internal/logger"
"github.com/gabehf/koito/internal/models"
"github.com/gabehf/koito/internal/utils"
"github.com/google/uuid"
)
type MiddlwareContextKey string
const (
UserContextKey MiddlwareContextKey = "user"
apikeyContextKey MiddlwareContextKey = "apikeyID"
)
func ValidateSession(store db.DB) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
l := logger.FromContext(r.Context())
l.Debug().Msgf("ValidateSession: Checking user authentication via session cookie")
cookie, err := r.Cookie("koito_session")
var sid uuid.UUID
if err == nil {
sid, err = uuid.Parse(cookie.Value)
if err != nil {
l.Err(err).Msg("ValidateSession: Could not parse UUID from session cookie")
utils.WriteError(w, "session cookie is invalid", http.StatusUnauthorized)
return
}
} else {
l.Debug().Msgf("ValidateSession: No session cookie found; attempting API key authentication")
utils.WriteError(w, "session cookie is missing", http.StatusUnauthorized)
return
}
l.Debug().Msg("ValidateSession: Retrieved login cookie from request")
u, err := store.GetUserBySession(r.Context(), sid)
if err != nil {
l.Err(fmt.Errorf("ValidateSession: %w", err)).Msg("Error accessing database")
utils.WriteError(w, "internal server error", http.StatusInternalServerError)
return
}
if u == nil {
l.Debug().Msg("ValidateSession: No user with session id found")
utils.WriteError(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), UserContextKey, u)
r = r.WithContext(ctx)
l.Debug().Msgf("ValidateSession: Refreshing session for user '%s'", u.Username)
store.RefreshSession(r.Context(), sid, time.Now().Add(30*24*time.Hour))
l.Debug().Msgf("ValidateSession: Refreshed session for user '%s'", u.Username)
next.ServeHTTP(w, r)
})
}
}
func ValidateApiKey(store db.DB) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
l := logger.FromContext(ctx)
l.Debug().Msg("ValidateApiKey: Checking if user is already authenticated")
u := GetUserFromContext(ctx)
if u != nil {
l.Debug().Msg("ValidateApiKey: User is already authenticated; skipping API key authentication")
next.ServeHTTP(w, r)
return
}
authh := r.Header.Get("Authorization")
var token string
if strings.HasPrefix(strings.ToLower(authh), "token ") {
token = strings.TrimSpace(authh[6:]) // strip "Token "
} else {
l.Error().Msg("ValidateApiKey: Authorization header must be formatted 'Token {token}'")
utils.WriteError(w, "unauthorized", http.StatusUnauthorized)
return
}
u, err := store.GetUserByApiKey(ctx, token)
if err != nil {
l.Err(err).Msg("Failed to get user from database using api key")
utils.WriteError(w, "internal server error", http.StatusInternalServerError)
return
}
if u == nil {
l.Debug().Msg("Api key does not exist")
utils.WriteError(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx = context.WithValue(r.Context(), UserContextKey, u)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}
func GetUserFromContext(ctx context.Context) *models.User {
user, ok := ctx.Value(UserContextKey).(*models.User)
if !ok {
return nil
}
return user
}

View file

@ -38,9 +38,7 @@ func bindRoutes(
r.Get("/config", handlers.GetCfgHandler()) r.Get("/config", handlers.GetCfgHandler())
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
if cfg.LoginGate() { r.Use(middleware.Authenticate(db, middleware.AuthModeLoginGate))
r.Use(middleware.ValidateSession(db))
}
r.Get("/artist", handlers.GetArtistHandler(db)) r.Get("/artist", handlers.GetArtistHandler(db))
r.Get("/artists", handlers.GetArtistsForItemHandler(db)) r.Get("/artists", handlers.GetArtistsForItemHandler(db))
r.Get("/album", handlers.GetAlbumHandler(db)) r.Get("/album", handlers.GetAlbumHandler(db))
@ -79,7 +77,7 @@ func bindRoutes(
}) })
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
r.Use(middleware.ValidateSession(db)) r.Use(middleware.Authenticate(db, middleware.AuthModeSessionOrAPIKey))
r.Get("/export", handlers.ExportHandler(db)) r.Get("/export", handlers.ExportHandler(db))
r.Post("/replace-image", handlers.ReplaceImageHandler(db)) r.Post("/replace-image", handlers.ReplaceImageHandler(db))
r.Patch("/album", handlers.UpdateAlbumHandler(db)) r.Patch("/album", handlers.UpdateAlbumHandler(db))
@ -111,8 +109,10 @@ func bindRoutes(
AllowedHeaders: []string{"Content-Type", "Authorization"}, AllowedHeaders: []string{"Content-Type", "Authorization"},
})) }))
r.With(middleware.ValidateApiKey(db)).Post("/submit-listens", handlers.LbzSubmitListenHandler(db, mbz)) r.With(middleware.Authenticate(db, middleware.AuthModeAPIKey)).
r.With(middleware.ValidateApiKey(db)).Get("/validate-token", handlers.LbzValidateTokenHandler(db)) Post("/submit-listens", handlers.LbzSubmitListenHandler(db, mbz))
r.With(middleware.Authenticate(db, middleware.AuthModeAPIKey)).
Get("/validate-token", handlers.LbzValidateTokenHandler(db))
}) })
// serve react client // serve react client

View file

@ -244,204 +244,3 @@ func parseBool(s string) bool {
return false return false
} }
} }
// Global accessors for configuration values
func UserAgent() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.userAgent
}
func ListenAddr() string {
lock.RLock()
defer lock.RUnlock()
return fmt.Sprintf("%s:%d", globalConfig.bindAddr, globalConfig.listenPort)
}
func ConfigDir() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.configDir
}
func DatabaseUrl() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.databaseUrl
}
func MusicBrainzUrl() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.musicBrainzUrl
}
func MusicBrainzRateLimit() int {
lock.RLock()
defer lock.RUnlock()
return globalConfig.musicBrainzRateLimit
}
func LogLevel() int {
lock.RLock()
defer lock.RUnlock()
return globalConfig.logLevel
}
func StructuredLogging() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.structuredLogging
}
func LbzRelayEnabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.lbzRelayEnabled
}
func LbzRelayUrl() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.lbzRelayUrl
}
func LbzRelayToken() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.lbzRelayToken
}
func DefaultPassword() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.defaultPw
}
func DefaultUsername() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.defaultUsername
}
func DefaultTheme() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.defaultTheme
}
func FullImageCacheEnabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.enableFullImageCache
}
func DeezerDisabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.disableDeezer
}
func CoverArtArchiveDisabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.disableCAA
}
func MusicBrainzDisabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.disableMusicBrainz
}
func SubsonicEnabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.subsonicEnabled
}
func SubsonicUrl() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.subsonicUrl
}
func SubsonicParams() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.subsonicParams
}
func LastFMApiKey() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.lastfmApiKey
}
func SkipImport() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.skipImport
}
func AllowedHosts() []string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.allowedHosts
}
func AllowAllHosts() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.allowAllHosts
}
func AllowedOrigins() []string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.allowedOrigins
}
func RateLimitDisabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.disableRateLimit
}
func ThrottleImportMs() int {
lock.RLock()
defer lock.RUnlock()
return globalConfig.importThrottleMs
}
// returns the before, after times, in that order
func ImportWindow() (time.Time, time.Time) {
lock.RLock()
defer lock.RUnlock()
return globalConfig.importBefore, globalConfig.importAfter
}
func FetchImagesDuringImport() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.fetchImageDuringImport
}
func ArtistSeparators() []*regexp.Regexp {
lock.RLock()
defer lock.RUnlock()
return globalConfig.artistSeparators
}
func LoginGate() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.loginGate
}
func ForceTZ() *time.Location {
lock.RLock()
defer lock.RUnlock()
return globalConfig.forceTZ
}

206
internal/cfg/getters.go Normal file
View file

@ -0,0 +1,206 @@
package cfg
import (
"fmt"
"regexp"
"time"
)
func UserAgent() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.userAgent
}
func ListenAddr() string {
lock.RLock()
defer lock.RUnlock()
return fmt.Sprintf("%s:%d", globalConfig.bindAddr, globalConfig.listenPort)
}
func ConfigDir() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.configDir
}
func DatabaseUrl() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.databaseUrl
}
func MusicBrainzUrl() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.musicBrainzUrl
}
func MusicBrainzRateLimit() int {
lock.RLock()
defer lock.RUnlock()
return globalConfig.musicBrainzRateLimit
}
func LogLevel() int {
lock.RLock()
defer lock.RUnlock()
return globalConfig.logLevel
}
func StructuredLogging() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.structuredLogging
}
func LbzRelayEnabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.lbzRelayEnabled
}
func LbzRelayUrl() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.lbzRelayUrl
}
func LbzRelayToken() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.lbzRelayToken
}
func DefaultPassword() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.defaultPw
}
func DefaultUsername() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.defaultUsername
}
func DefaultTheme() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.defaultTheme
}
func FullImageCacheEnabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.enableFullImageCache
}
func DeezerDisabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.disableDeezer
}
func CoverArtArchiveDisabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.disableCAA
}
func MusicBrainzDisabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.disableMusicBrainz
}
func SubsonicEnabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.subsonicEnabled
}
func SubsonicUrl() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.subsonicUrl
}
func SubsonicParams() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.subsonicParams
}
func LastFMApiKey() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.lastfmApiKey
}
func SkipImport() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.skipImport
}
func AllowedHosts() []string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.allowedHosts
}
func AllowAllHosts() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.allowAllHosts
}
func AllowedOrigins() []string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.allowedOrigins
}
func RateLimitDisabled() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.disableRateLimit
}
func ThrottleImportMs() int {
lock.RLock()
defer lock.RUnlock()
return globalConfig.importThrottleMs
}
// returns the before, after times, in that order
func ImportWindow() (time.Time, time.Time) {
lock.RLock()
defer lock.RUnlock()
return globalConfig.importBefore, globalConfig.importAfter
}
func FetchImagesDuringImport() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.fetchImageDuringImport
}
func ArtistSeparators() []*regexp.Regexp {
lock.RLock()
defer lock.RUnlock()
return globalConfig.artistSeparators
}
func LoginGate() bool {
lock.RLock()
defer lock.RUnlock()
return globalConfig.loginGate
}
func ForceTZ() *time.Location {
lock.RLock()
defer lock.RUnlock()
return globalConfig.forceTZ
}

7
internal/cfg/setters.go Normal file
View file

@ -0,0 +1,7 @@
package cfg
func SetLoginGate(val bool) {
lock.Lock()
defer lock.Unlock()
globalConfig.loginGate = val
}

View file

@ -85,7 +85,14 @@ func ImportListenBrainzFile(ctx context.Context, store db.DB, mbzc mbz.MusicBrai
} }
artistMbzIDs, err := utils.ParseUUIDSlice(payload.TrackMeta.AdditionalInfo.ArtistMBIDs) artistMbzIDs, err := utils.ParseUUIDSlice(payload.TrackMeta.AdditionalInfo.ArtistMBIDs)
if err != nil { if err != nil {
l.Debug().Err(err).Msg("Failed to parse one or more uuids") l.Debug().AnErr("error", err).Msg("ImportListenBrainzFile: Failed to parse one or more UUIDs")
}
if len(artistMbzIDs) < 1 {
l.Debug().AnErr("error", err).Msg("ImportListenBrainzFile: Attempting to parse artist UUIDs from mbid_mapping")
utils.ParseUUIDSlice(payload.TrackMeta.MBIDMapping.ArtistMBIDs)
if err != nil {
l.Debug().AnErr("error", err).Msg("ImportListenBrainzFile: Failed to parse one or more UUIDs")
}
} }
rgMbzID, err := uuid.Parse(payload.TrackMeta.AdditionalInfo.ReleaseGroupMBID) rgMbzID, err := uuid.Parse(payload.TrackMeta.AdditionalInfo.ReleaseGroupMBID)
if err != nil { if err != nil {
@ -93,11 +100,17 @@ func ImportListenBrainzFile(ctx context.Context, store db.DB, mbzc mbz.MusicBrai
} }
releaseMbzID, err := uuid.Parse(payload.TrackMeta.AdditionalInfo.ReleaseMBID) releaseMbzID, err := uuid.Parse(payload.TrackMeta.AdditionalInfo.ReleaseMBID)
if err != nil { if err != nil {
releaseMbzID = uuid.Nil releaseMbzID, err = uuid.Parse(payload.TrackMeta.MBIDMapping.ReleaseMBID)
if err != nil {
releaseMbzID = uuid.Nil
}
} }
recordingMbzID, err := uuid.Parse(payload.TrackMeta.AdditionalInfo.RecordingMBID) recordingMbzID, err := uuid.Parse(payload.TrackMeta.AdditionalInfo.RecordingMBID)
if err != nil { if err != nil {
recordingMbzID = uuid.Nil recordingMbzID, err = uuid.Parse(payload.TrackMeta.MBIDMapping.RecordingMBID)
if err != nil {
recordingMbzID = uuid.Nil
}
} }
var client string var client string

Binary file not shown.