Compare commits

..

11 Commits

Author SHA1 Message Date
Gabe Farrell 6cb655166f test: add integration test
3 weeks ago
Gabe Farrell 52e955b1b6 fix: ensure timestamp is in the past, log fix
3 weeks ago
Gabe Farrell b4ce44c658 feat: add button to manually scrobble from ui
3 weeks ago
Gabe Farrell 1be573e720 docs: add default theme cfg option to docs
3 weeks ago
Gabe Farrell 1aeb6408aa
feat: add server-side configuration with default theme (#90)
3 weeks ago
Gabe Farrell 70f5198781
fix: set first artist listed as primary by default (#81)
1 month ago
mlandry cd31b6f2d8
fix: race condition with using getComputedStyle primary color for dynamic activity grid darkening (#76)
2 months ago
Gabe Farrell 59f715120f fix dev container push workflow
2 months ago
pet ccaed823e2
fix: set name/short_name to koito (#61)
2 months ago
Gabe Farrell abd4de319e correctly set the default range of ActivityGrid
2 months ago
Gabe Farrell 986b71dcbc add dev branch container to workflow
2 months ago

@ -13,6 +13,8 @@ on:
push: push:
tags: tags:
- 'v*' - 'v*'
branches:
- dev
workflow_dispatch: workflow_dispatch:
@ -21,42 +23,37 @@ jobs:
name: Go Test name: Go Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version-file: go.mod go-version-file: go.mod
- name: Install libvips - name: Install libvips
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libvips-dev sudo apt-get install -y libvips-dev
- name: Verify libvips install - name: Verify libvips install
run: vips --version run: vips --version
- name: Build - name: Build
run: go build -v ./... run: go build -v ./...
- name: Test - name: Test
uses: robherley/go-test-action@v0 uses: robherley/go-test-action@v0
push_to_registry: push_to_registry:
name: Push Docker image to Docker Hub name: Push Docker image to Docker Hub (release)
if: startsWith(github.ref, 'refs/tags/')
needs: test needs: test
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
packages: write
contents: read
attestations: write
id-token: write
steps: steps:
- name: Check out the repo - uses: actions/checkout@v4
uses: actions/checkout@v4
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }} password: ${{ secrets.DOCKER_TOKEN }}
@ -64,19 +61,12 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: gabehf/koito
- name: Extract tag version - name: Extract tag version
id: extract_version
run: echo "KOITO_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV run: echo "KOITO_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Build and push Docker image - name: Build and push release image
id: push id: push
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
@ -88,9 +78,32 @@ jobs:
KOITO_VERSION=${{ env.KOITO_VERSION }} KOITO_VERSION=${{ env.KOITO_VERSION }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
- name: Generate artifact attestation push_dev:
uses: actions/attest-build-provenance@v2 name: Push Docker image (dev branch)
if: github.ref == 'refs/heads/dev'
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
with: with:
subject-name: index.docker.io/gabehf/koito username: ${{ secrets.DOCKER_USERNAME }}
subject-digest: ${{ steps.push.outputs.digest }} password: ${{ secrets.DOCKER_TOKEN }}
push-to-registry: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push dev image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: |
gabehf/koito:dev
gabehf/koito:dev-${{ github.sha }}
build-args: |
KOITO_VERSION=dev
platforms: linux/amd64,linux/arm64

@ -101,6 +101,11 @@ function logout(): Promise<Response> {
}) })
} }
function getCfg(): Promise<Config> {
return fetch(`/apis/web/v1/config`).then(r => r.json() as Promise<Config>)
}
function submitListen(id: string, ts: Date): Promise<Response> { function submitListen(id: string, ts: Date): Promise<Response> {
const form = new URLSearchParams const form = new URLSearchParams
form.append("track_id", id) form.append("track_id", id)
@ -226,6 +231,7 @@ export {
imageUrl, imageUrl,
login, login,
logout, logout,
getCfg,
deleteItem, deleteItem,
updateUser, updateUser,
getAliases, getAliases,
@ -322,6 +328,9 @@ type ApiKey = {
type ApiError = { type ApiError = {
error: string error: string
} }
type Config = {
default_theme: string
}
export type { export type {
getItemsArgs, getItemsArgs,
@ -336,5 +345,6 @@ export type {
User, User,
Alias, Alias,
ApiKey, ApiKey,
ApiError ApiError,
Config
} }

@ -43,7 +43,8 @@ export default function ActivityOptsSelector({
useEffect(() => { useEffect(() => {
if (!disableCache) { if (!disableCache) {
const cachedRange = parseInt(localStorage.getItem('activity_range_' + window.location.pathname.split('/')[1]) ?? '35'); // TODO: the '182' here overwrites the default range as configured in the ActivityGrid. This is bad. Only one of these should determine the default.
const cachedRange = parseInt(localStorage.getItem('activity_range_' + window.location.pathname.split('/')[1]) ?? '182');
if (cachedRange) rangeSetter(cachedRange); if (cachedRange) rangeSetter(cachedRange);
const cachedStep = localStorage.getItem('activity_step_' + window.location.pathname.split('/')[1]); const cachedStep = localStorage.getItem('activity_step_' + window.location.pathname.split('/')[1]);
if (cachedStep) stepSetter(cachedStep); if (cachedStep) stepSetter(cachedStep);

@ -1,12 +1,11 @@
// ThemeSwitcher.tsx import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useTheme } from '../../hooks/useTheme'; import { useTheme } from '../../hooks/useTheme';
import themes from '~/styles/themes.css'; import themes from '~/styles/themes.css';
import ThemeOption from './ThemeOption'; import ThemeOption from './ThemeOption';
import { AsyncButton } from '../AsyncButton'; import { AsyncButton } from '../AsyncButton';
export function ThemeSwitcher() { export function ThemeSwitcher() {
const { theme, themeName, setTheme } = useTheme(); const { setTheme } = useTheme();
const initialTheme = { const initialTheme = {
bg: "#1e1816", bg: "#1e1816",
bgSecondary: "#2f2623", bgSecondary: "#2f2623",
@ -24,7 +23,7 @@ export function ThemeSwitcher() {
info: "#87b8dd", info: "#87b8dd",
} }
const { setCustomTheme, getCustomTheme } = useTheme() const { setCustomTheme, getCustomTheme, resetTheme } = useTheme()
const [custom, setCustom] = useState(JSON.stringify(getCustomTheme() ?? initialTheme, null, " ")) const [custom, setCustom] = useState(JSON.stringify(getCustomTheme() ?? initialTheme, null, " "))
const handleCustomTheme = () => { const handleCustomTheme = () => {
@ -42,7 +41,12 @@ export function ThemeSwitcher() {
return ( return (
<div className='flex flex-col gap-10'> <div className='flex flex-col gap-10'>
<div> <div>
<h2>Select Theme</h2> <div className='flex items-center gap-3'>
<h2>Select Theme</h2>
<div className='mb-3'>
<AsyncButton onClick={resetTheme}>Reset</AsyncButton>
</div>
</div>
<div className="grid grid-cols-2 items-center gap-2"> <div className="grid grid-cols-2 items-center gap-2">
{Object.entries(themes).map(([name, themeData]) => ( {Object.entries(themes).map(([name, themeData]) => (
<ThemeOption setTheme={setTheme} key={name} theme={themeData} themeName={name} /> <ThemeOption setTheme={setTheme} key={name} theme={themeData} themeName={name} />

@ -1,10 +1,11 @@
import type { User } from "api/api"; import { getCfg, type User } from "api/api";
import { createContext, useContext, useEffect, useState } from "react"; import { createContext, useContext, useEffect, useState } from "react";
interface AppContextType { interface AppContextType {
user: User | null | undefined; user: User | null | undefined;
configurableHomeActivity: boolean; configurableHomeActivity: boolean;
homeItems: number; homeItems: number;
defaultTheme: string;
setConfigurableHomeActivity: (value: boolean) => void; setConfigurableHomeActivity: (value: boolean) => void;
setHomeItems: (value: number) => void; setHomeItems: (value: number) => void;
setUsername: (value: string) => void; setUsername: (value: string) => void;
@ -22,6 +23,7 @@ export const useAppContext = () => {
export const AppProvider = ({ children }: { children: React.ReactNode }) => { export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState<User | null | undefined>(undefined); const [user, setUser] = useState<User | null | undefined>(undefined);
const [defaultTheme, setDefaultTheme] = useState<string | undefined>(undefined)
const [configurableHomeActivity, setConfigurableHomeActivity] = useState<boolean>(false); const [configurableHomeActivity, setConfigurableHomeActivity] = useState<boolean>(false);
const [homeItems, setHomeItems] = useState<number>(0); const [homeItems, setHomeItems] = useState<number>(0);
@ -42,9 +44,15 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
setConfigurableHomeActivity(true); setConfigurableHomeActivity(true);
setHomeItems(12); setHomeItems(12);
getCfg().then(cfg => {
console.log(cfg)
setDefaultTheme(cfg.default_theme)
})
}, []); }, []);
if (user === undefined) { // Block rendering the app until config is loaded
if (user === undefined || defaultTheme === undefined) {
return null; return null;
} }
@ -52,6 +60,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
user, user,
configurableHomeActivity, configurableHomeActivity,
homeItems, homeItems,
defaultTheme,
setConfigurableHomeActivity, setConfigurableHomeActivity,
setHomeItems, setHomeItems,
setUsername, setUsername,

@ -1,11 +1,13 @@
import { createContext, useEffect, useState, useCallback, type ReactNode } from 'react'; import { createContext, useEffect, useState, useCallback, type ReactNode } from 'react';
import { type Theme, themes } from '~/styles/themes.css'; import { type Theme, themes } from '~/styles/themes.css';
import { themeVars } from '~/styles/vars.css'; import { themeVars } from '~/styles/vars.css';
import { useAppContext } from './AppProvider';
interface ThemeContextValue { interface ThemeContextValue {
themeName: string; themeName: string;
theme: Theme; theme: Theme;
setTheme: (theme: string) => void; setTheme: (theme: string) => void;
resetTheme: () => void;
setCustomTheme: (theme: Theme) => void; setCustomTheme: (theme: Theme) => void;
getCustomTheme: () => Theme | undefined; getCustomTheme: () => Theme | undefined;
} }
@ -43,19 +45,19 @@ function getStoredCustomTheme(): Theme | undefined {
} }
export function ThemeProvider({ export function ThemeProvider({
theme: initialTheme,
children, children,
}: { }: {
theme: string;
children: ReactNode; children: ReactNode;
}) { }) {
let defaultTheme = useAppContext().defaultTheme
let initialTheme = localStorage.getItem("theme") ?? defaultTheme
const [themeName, setThemeName] = useState(initialTheme); const [themeName, setThemeName] = useState(initialTheme);
const [currentTheme, setCurrentTheme] = useState<Theme>(() => { const [currentTheme, setCurrentTheme] = useState<Theme>(() => {
if (initialTheme === 'custom') { if (initialTheme === 'custom') {
const customTheme = getStoredCustomTheme(); const customTheme = getStoredCustomTheme();
return customTheme || themes.yuu; return customTheme || themes[defaultTheme];
} }
return themes[initialTheme] || themes.yuu; return themes[initialTheme] || themes[defaultTheme];
}); });
const setTheme = (newThemeName: string) => { const setTheme = (newThemeName: string) => {
@ -66,21 +68,29 @@ export function ThemeProvider({
setCurrentTheme(customTheme); setCurrentTheme(customTheme);
} else { } else {
// Fallback to default theme if no custom theme found // Fallback to default theme if no custom theme found
setThemeName('yuu'); setThemeName(defaultTheme);
setCurrentTheme(themes.yuu); setCurrentTheme(themes[defaultTheme]);
} }
} else { } else {
const foundTheme = themes[newThemeName]; const foundTheme = themes[newThemeName];
if (foundTheme) { if (foundTheme) {
localStorage.setItem('theme', newThemeName)
setCurrentTheme(foundTheme); setCurrentTheme(foundTheme);
} }
} }
} }
const resetTheme = () => {
setThemeName(defaultTheme)
localStorage.removeItem('theme')
setCurrentTheme(themes[defaultTheme])
}
const setCustomTheme = useCallback((customTheme: Theme) => { const setCustomTheme = useCallback((customTheme: Theme) => {
localStorage.setItem('custom-theme', JSON.stringify(customTheme)); localStorage.setItem('custom-theme', JSON.stringify(customTheme));
applyCustomThemeVars(customTheme); applyCustomThemeVars(customTheme);
setThemeName('custom'); setThemeName('custom');
localStorage.setItem('theme', 'custom')
setCurrentTheme(customTheme); setCurrentTheme(customTheme);
}, []); }, []);
@ -92,7 +102,6 @@ export function ThemeProvider({
const root = document.documentElement; const root = document.documentElement;
root.setAttribute('data-theme', themeName); root.setAttribute('data-theme', themeName);
localStorage.setItem('theme', themeName);
if (themeName === 'custom') { if (themeName === 'custom') {
applyCustomThemeVars(currentTheme); applyCustomThemeVars(currentTheme);
@ -102,7 +111,14 @@ export function ThemeProvider({
}, [themeName, currentTheme]); }, [themeName, currentTheme]);
return ( return (
<ThemeContext.Provider value={{ themeName, theme: currentTheme, setTheme, setCustomTheme, getCustomTheme }}> <ThemeContext.Provider value={{
themeName,
theme: currentTheme,
setTheme,
resetTheme,
setCustomTheme,
getCustomTheme
}}>
{children} {children}
</ThemeContext.Provider> </ThemeContext.Provider>
); );

@ -58,12 +58,10 @@ export function Layout({ children }: { children: React.ReactNode }) {
} }
export default function App() { export default function App() {
let theme = localStorage.getItem('theme') ?? 'yuu'
return ( return (
<> <>
<AppProvider> <AppProvider>
<ThemeProvider theme={theme}> <ThemeProvider>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<div className="flex-col flex sm:flex-row"> <div className="flex-col flex sm:flex-row">
<Sidebar /> <Sidebar />
@ -99,18 +97,12 @@ export function ErrorBoundary() {
stack = error.stack; stack = error.stack;
} }
let theme = 'yuu'
try {
theme = localStorage.getItem('theme') ?? theme
} catch(err) {
console.log(err)
}
const title = `${message} - Koito` const title = `${message} - Koito`
return ( return (
<AppProvider> <AppProvider>
<ThemeProvider theme={theme}> <ThemeProvider>
<title>{title}</title> <title>{title}</title>
<div className="flex"> <div className="flex">
<Sidebar /> <Sidebar />

@ -1,6 +1,6 @@
{ {
"name": "MyWebSite", "name": "Koito",
"short_name": "MySite", "short_name": "Koito",
"icons": [ "icons": [
{ {
"src": "/web-app-manifest-192x192.png", "src": "/web-app-manifest-192x192.png",

@ -74,8 +74,8 @@ JOIN artist_releases ar ON r.id = ar.release_id
WHERE ar.artist_id = $1; WHERE ar.artist_id = $1;
-- name: AssociateArtistToRelease :exec -- name: AssociateArtistToRelease :exec
INSERT INTO artist_releases (artist_id, release_id) INSERT INTO artist_releases (artist_id, release_id, is_primary)
VALUES ($1, $2) VALUES ($1, $2, $3)
ON CONFLICT DO NOTHING; ON CONFLICT DO NOTHING;
-- name: GetReleasesWithoutImages :many -- name: GetReleasesWithoutImages :many

@ -4,8 +4,8 @@ VALUES ($1, $2, $3)
RETURNING *; RETURNING *;
-- name: AssociateArtistToTrack :exec -- name: AssociateArtistToTrack :exec
INSERT INTO artist_tracks (artist_id, track_id) INSERT INTO artist_tracks (artist_id, track_id, is_primary)
VALUES ($1, $2) VALUES ($1, $2, $3)
ON CONFLICT DO NOTHING; ON CONFLICT DO NOTHING;
-- name: GetTrack :one -- name: GetTrack :one

@ -23,6 +23,9 @@ If the environment variable is defined without **and** with the suffix at the sa
##### KOITO_DEFAULT_PASSWORD ##### KOITO_DEFAULT_PASSWORD
- Default: `changeme` - Default: `changeme`
- Description: The password for the user that is created on first startup. Only applies when running Koito for the first time. - Description: The password for the user that is created on first startup. Only applies when running Koito for the first time.
##### KOITO_DEFAULT_THEME
- Default: `yuu`
- Description: The lowercase name of the default theme to be used by the client. Overridden if a user picks a theme in the theme switcher.
##### KOITO_BIND_ADDR ##### KOITO_BIND_ADDR
- Description: The address to bind to. The default blank value is equivalent to `0.0.0.0`. - Description: The address to bind to. The default blank value is equivalent to `0.0.0.0`.
##### KOITO_LISTEN_PORT ##### KOITO_LISTEN_PORT

@ -36,7 +36,7 @@ func SubmitListenWithIDHandler(store db.DB) http.HandlerFunc {
} }
trackIDStr := r.FormValue("track_id") trackIDStr := r.FormValue("track_id")
timestampStr := r.FormValue("unix") // unix timestampStr := r.FormValue("unix")
client := r.FormValue("client") client := r.FormValue("client")
if client == "" { if client == "" {
client = defaultClientStr client = defaultClientStr
@ -54,8 +54,8 @@ func SubmitListenWithIDHandler(store db.DB) http.HandlerFunc {
return return
} }
unix, err := strconv.ParseInt(timestampStr, 10, 64) unix, err := strconv.ParseInt(timestampStr, 10, 64)
if err != nil { if err != nil || time.Now().Unix() < unix {
l.Debug().AnErr("error", err).Msg("SubmitListenWithIDHandler: Invalid track id") l.Debug().AnErr("error", err).Msg("SubmitListenWithIDHandler: Invalid unix timestamp")
utils.WriteError(w, "invalid timestamp", http.StatusBadRequest) utils.WriteError(w, "invalid timestamp", http.StatusBadRequest)
return return
} }

@ -0,0 +1,18 @@
package handlers
import (
"net/http"
"github.com/gabehf/koito/internal/cfg"
"github.com/gabehf/koito/internal/utils"
)
type ServerConfig struct {
DefaultTheme string `json:"default_theme"`
}
func GetCfgHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
utils.WriteJSON(w, http.StatusOK, ServerConfig{DefaultTheme: cfg.DefaultTheme()})
}
}

@ -11,6 +11,7 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"strconv"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@ -890,3 +891,29 @@ func TestSetPrimaryArtist(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.EqualValues(t, 1, count, "expected only one primary artist for track") assert.EqualValues(t, 1, count, "expected only one primary artist for track")
} }
func TestManualListen(t *testing.T) {
t.Run("Submit Listens", doSubmitListens)
ctx := context.Background()
// happy
formdata := url.Values{}
formdata.Set("track_id", "1")
formdata.Set("unix", strconv.FormatInt(time.Now().Unix()-60, 10))
body := formdata.Encode()
resp, err := makeAuthRequest(t, session, "POST", "/apis/web/v1/listen", strings.NewReader(body))
require.NoError(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
count, _ := store.Count(ctx, `SELECT COUNT(*) FROM listens WHERE track_id = $1`, 1)
assert.Equal(t, 2, count)
// 400
formdata.Set("track_id", "1")
formdata.Set("unix", strconv.FormatInt(time.Now().Unix()+60, 10))
body = formdata.Encode()
resp, err = makeAuthRequest(t, session, "POST", "/apis/web/v1/listen", strings.NewReader(body))
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}

@ -35,6 +35,7 @@ func bindRoutes(
Get("/images/{size}/{filename}", handlers.ImageHandler(db)) Get("/images/{size}/{filename}", handlers.ImageHandler(db))
r.Route("/apis/web/v1", func(r chi.Router) { r.Route("/apis/web/v1", func(r chi.Router) {
r.Get("/config", handlers.GetCfgHandler())
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))

@ -203,6 +203,22 @@ func TestSubmitListen_CreateAllNoMbzIDsNoArtistNamesNoReleaseTitle(t *testing.T)
)`, "Madeline Kenney") )`, "Madeline Kenney")
require.NoError(t, err) require.NoError(t, err)
assert.True(t, exists, "expected featured artist to be created") assert.True(t, exists, "expected featured artist to be created")
// assert that Rat Tally is the primary artist
exists, err = store.RowExists(ctx, `
SELECT EXISTS (
SELECT 1 FROM artist_tracks
WHERE artist_id = $1 AND is_primary = $2
)`, 1, true)
require.NoError(t, err)
assert.True(t, exists, "expected primary artist to be marked as primary for track")
exists, err = store.RowExists(ctx, `
SELECT EXISTS (
SELECT 1 FROM artist_releases
WHERE artist_id = $1 AND is_primary = $2
)`, 1, true)
require.NoError(t, err)
assert.True(t, exists, "expected primary artist to be marked as primary for release")
} }
func TestSubmitListen_MatchAllMbzIDs(t *testing.T) { func TestSubmitListen_MatchAllMbzIDs(t *testing.T) {

@ -31,6 +31,7 @@ const (
CONFIG_DIR_ENV = "KOITO_CONFIG_DIR" CONFIG_DIR_ENV = "KOITO_CONFIG_DIR"
DEFAULT_USERNAME_ENV = "KOITO_DEFAULT_USERNAME" DEFAULT_USERNAME_ENV = "KOITO_DEFAULT_USERNAME"
DEFAULT_PASSWORD_ENV = "KOITO_DEFAULT_PASSWORD" DEFAULT_PASSWORD_ENV = "KOITO_DEFAULT_PASSWORD"
DEFAULT_THEME_ENV = "KOITO_DEFAULT_THEME"
DISABLE_DEEZER_ENV = "KOITO_DISABLE_DEEZER" DISABLE_DEEZER_ENV = "KOITO_DISABLE_DEEZER"
DISABLE_COVER_ART_ARCHIVE_ENV = "KOITO_DISABLE_COVER_ART_ARCHIVE" DISABLE_COVER_ART_ARCHIVE_ENV = "KOITO_DISABLE_COVER_ART_ARCHIVE"
DISABLE_MUSICBRAINZ_ENV = "KOITO_DISABLE_MUSICBRAINZ" DISABLE_MUSICBRAINZ_ENV = "KOITO_DISABLE_MUSICBRAINZ"
@ -60,6 +61,7 @@ type config struct {
lbzRelayToken string lbzRelayToken string
defaultPw string defaultPw string
defaultUsername string defaultUsername string
defaultTheme string
disableDeezer bool disableDeezer bool
disableCAA bool disableCAA bool
disableMusicBrainz bool disableMusicBrainz bool
@ -162,6 +164,8 @@ func loadConfig(getenv func(string) string, version string) (*config, error) {
cfg.defaultPw = getenv(DEFAULT_PASSWORD_ENV) cfg.defaultPw = getenv(DEFAULT_PASSWORD_ENV)
} }
cfg.defaultTheme = getenv(DEFAULT_THEME_ENV)
cfg.configDir = getenv(CONFIG_DIR_ENV) cfg.configDir = getenv(CONFIG_DIR_ENV)
if cfg.configDir == "" { if cfg.configDir == "" {
cfg.configDir = "/etc/koito" cfg.configDir = "/etc/koito"
@ -277,6 +281,12 @@ func DefaultUsername() string {
return globalConfig.defaultUsername return globalConfig.defaultUsername
} }
func DefaultTheme() string {
lock.RLock()
defer lock.RUnlock()
return globalConfig.defaultTheme
}
func FullImageCacheEnabled() bool { func FullImageCacheEnabled() bool {
lock.RLock() lock.RLock()
defer lock.RUnlock() defer lock.RUnlock()

@ -144,6 +144,7 @@ func (d *Psql) SaveAlbum(ctx context.Context, opts db.SaveAlbumOpts) (*models.Al
err = qtx.AssociateArtistToRelease(ctx, repository.AssociateArtistToReleaseParams{ err = qtx.AssociateArtistToRelease(ctx, repository.AssociateArtistToReleaseParams{
ArtistID: artistId, ArtistID: artistId,
ReleaseID: r.ID, ReleaseID: r.ID,
IsPrimary: opts.ArtistIDs[0] == artistId,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("SaveAlbum: AssociateArtistToRelease: %w", err) return nil, fmt.Errorf("SaveAlbum: AssociateArtistToRelease: %w", err)

@ -132,8 +132,9 @@ func (d *Psql) SaveTrack(ctx context.Context, opts db.SaveTrackOpts) (*models.Tr
// insert associated artists // insert associated artists
for _, aid := range opts.ArtistIDs { for _, aid := range opts.ArtistIDs {
err = qtx.AssociateArtistToTrack(ctx, repository.AssociateArtistToTrackParams{ err = qtx.AssociateArtistToTrack(ctx, repository.AssociateArtistToTrackParams{
ArtistID: aid, ArtistID: aid,
TrackID: trackRow.ID, TrackID: trackRow.ID,
IsPrimary: opts.ArtistIDs[0] == aid,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("SaveTrack: AssociateArtistToTrack: %w", err) return nil, fmt.Errorf("SaveTrack: AssociateArtistToTrack: %w", err)

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: alias.sql // source: alias.sql
package repository package repository

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: artist.sql // source: artist.sql
package repository package repository

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
package repository package repository

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: etc.sql // source: etc.sql
package repository package repository

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: listen.sql // source: listen.sql
package repository package repository

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
package repository package repository

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: release.sql // source: release.sql
package repository package repository
@ -14,18 +14,19 @@ import (
) )
const associateArtistToRelease = `-- name: AssociateArtistToRelease :exec const associateArtistToRelease = `-- name: AssociateArtistToRelease :exec
INSERT INTO artist_releases (artist_id, release_id) INSERT INTO artist_releases (artist_id, release_id, is_primary)
VALUES ($1, $2) VALUES ($1, $2, $3)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
` `
type AssociateArtistToReleaseParams struct { type AssociateArtistToReleaseParams struct {
ArtistID int32 ArtistID int32
ReleaseID int32 ReleaseID int32
IsPrimary bool
} }
func (q *Queries) AssociateArtistToRelease(ctx context.Context, arg AssociateArtistToReleaseParams) error { func (q *Queries) AssociateArtistToRelease(ctx context.Context, arg AssociateArtistToReleaseParams) error {
_, err := q.db.Exec(ctx, associateArtistToRelease, arg.ArtistID, arg.ReleaseID) _, err := q.db.Exec(ctx, associateArtistToRelease, arg.ArtistID, arg.ReleaseID, arg.IsPrimary)
return err return err
} }

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: search.sql // source: search.sql
package repository package repository

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: sessions.sql // source: sessions.sql
package repository package repository

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: track.sql // source: track.sql
package repository package repository
@ -13,18 +13,19 @@ import (
) )
const associateArtistToTrack = `-- name: AssociateArtistToTrack :exec const associateArtistToTrack = `-- name: AssociateArtistToTrack :exec
INSERT INTO artist_tracks (artist_id, track_id) INSERT INTO artist_tracks (artist_id, track_id, is_primary)
VALUES ($1, $2) VALUES ($1, $2, $3)
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
` `
type AssociateArtistToTrackParams struct { type AssociateArtistToTrackParams struct {
ArtistID int32 ArtistID int32
TrackID int32 TrackID int32
IsPrimary bool
} }
func (q *Queries) AssociateArtistToTrack(ctx context.Context, arg AssociateArtistToTrackParams) error { func (q *Queries) AssociateArtistToTrack(ctx context.Context, arg AssociateArtistToTrackParams) error {
_, err := q.db.Exec(ctx, associateArtistToTrack, arg.ArtistID, arg.TrackID) _, err := q.db.Exec(ctx, associateArtistToTrack, arg.ArtistID, arg.TrackID, arg.IsPrimary)
return err return err
} }

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: users.sql // source: users.sql
package repository package repository

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: year.sql // source: year.sql
package repository package repository

Loading…
Cancel
Save