init
This commit is contained in:
@@ -4,10 +4,22 @@ root = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.nix]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.ts]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.yaml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
node_modules
|
||||
result
|
||||
|
||||
4
config.yml
Normal file
4
config.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
version: 1
|
||||
base_url: "http://10.0.0.76:8096"
|
||||
system:
|
||||
enableMetrics: true
|
||||
20
esbuild.ts
Normal file
20
esbuild.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// esbuild.cjs
|
||||
const esbuild = require("esbuild");
|
||||
|
||||
async function main() {
|
||||
await esbuild.build({
|
||||
entryPoints: ["./src/cli.ts"],
|
||||
bundle: true,
|
||||
sourcemap: "inline",
|
||||
platform: "node",
|
||||
target: "node22",
|
||||
format: "cjs",
|
||||
outfile: "bundle.cjs",
|
||||
logLevel: "info",
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -40,7 +40,10 @@
|
||||
|
||||
formatter = (treefmt-nix.lib.evalModule pkgs ./treefmt.nix).config.build.wrapper;
|
||||
|
||||
packages.default = import ./nix/package.nix {inherit pkgs;};
|
||||
packages.default = import ./nix/package.nix {
|
||||
inherit pkgs;
|
||||
inherit (pkgs) lib;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
38898
generated/schema.d.ts
vendored
Normal file
38898
generated/schema.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
17
go.mod
17
go.mod
@@ -1,17 +0,0 @@
|
||||
module jellarr
|
||||
|
||||
go 1.24.6
|
||||
|
||||
require (
|
||||
github.com/onsi/gomega v1.38.2
|
||||
github.com/sj14/jellyfin-go v0.4.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
gopkg.in/validator.v2 v2.0.1 // indirect
|
||||
)
|
||||
39
go.sum
39
go.sum
@@ -1,39 +0,0 @@
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/onsi/ginkgo/v2 v2.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY=
|
||||
github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/sj14/jellyfin-go v0.4.1 h1:q+ra25LU+Ids6iLzEJZ4KFM46Z5gwz1XYZiCeZtik5c=
|
||||
github.com/sj14/jellyfin-go v0.4.1/go.mod h1:K3ozYgrTZF4403JWijLJyNDlGm+nkNZvfU5YRC+GYYc=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY=
|
||||
gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -1,24 +1,52 @@
|
||||
{pkgs}:
|
||||
pkgs.buildGoModule {
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
pkgs.stdenvNoCC.mkDerivation (finalAttrs: {
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
pnpm build
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
checkPhase = ''
|
||||
runHook preCheck
|
||||
export HOME="$TMPDIR"
|
||||
export GOCACHE="$TMPDIR/go-cache"
|
||||
go test ./src/cmd/jellarr/... ./src/tests/...
|
||||
pnpm test
|
||||
runHook postCheck
|
||||
'';
|
||||
doCheck = true;
|
||||
ldflags = ["-s" "-w"];
|
||||
meta = with pkgs.lib; {
|
||||
description = "Declarative Jellyfin configuration engine (typed Go client)";
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
install -Dm644 -t $out/share bundle.cjs
|
||||
makeWrapper ${lib.getExe pkgs.nodejs_24} $out/bin/jellarr \
|
||||
--add-flags "$out/share/bundle.cjs"
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Declarative Jellyfin configuration engine (TypeScript, bundled)";
|
||||
homepage = "https://github.com/venkyr77/jellarr";
|
||||
license = licenses.agpl3Only;
|
||||
license = lib.licenses.agpl3Only;
|
||||
mainProgram = "jellarr";
|
||||
platforms = platforms.linux ++ platforms.darwin;
|
||||
platforms = lib.platforms.all;
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.makeBinaryWrapper
|
||||
pkgs.nodejs_24
|
||||
pkgs.pnpm.configHook
|
||||
];
|
||||
|
||||
pname = "jellarr";
|
||||
|
||||
pnpmDeps = pkgs.pnpm.fetchDeps {
|
||||
fetcherVersion = 1;
|
||||
hash = "sha256-xhTEHi8UVeYD/OtTQMtFYk6SqX43+Tx73tCOoyCwOyw=";
|
||||
inherit (finalAttrs) pname src version;
|
||||
};
|
||||
|
||||
src = ../.;
|
||||
subPackages = ["src/cmd/jellarr"];
|
||||
vendorHash = "sha256-aIUKXAmLtq3bXesEVndQxLAFKmDmIWiEYhM1P6+IMKg=";
|
||||
|
||||
version = "0.1.0";
|
||||
}
|
||||
})
|
||||
|
||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.9.2",
|
||||
"@vitest/coverage-v8": "^4.0.6",
|
||||
"esbuild": "^0.25.11",
|
||||
"eslint": "^9.38.0",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^14.0.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"openapi-fetch": "^0.15.0",
|
||||
"undici": "^7.16.0",
|
||||
"yaml": "^2.8.1",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsx esbuild.ts",
|
||||
"dev": "tsx src/cli.ts",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typegen": "npx openapi-typescript https://api.jellyfin.org/openapi/jellyfin-openapi-stable.json -o ./generated/schema.d.ts"
|
||||
}
|
||||
}
|
||||
2401
pnpm-lock.yaml
generated
Normal file
2401
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
src/cli.ts
Normal file
18
src/cli.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
import { Command } from "commander";
|
||||
import { applySystem } from "./system";
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name("jellarr-ts")
|
||||
.description("Minimal Jellyfin config applier")
|
||||
.option("-c, --config <path>", "YAML config file", "config.yml")
|
||||
.action(async (opts) => {
|
||||
await applySystem(opts.config);
|
||||
});
|
||||
|
||||
program.parseAsync().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
22
src/client.ts
Normal file
22
src/client.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import createClient from "openapi-fetch";
|
||||
import type { paths } from "../generated/schema";
|
||||
|
||||
export function makeClient(baseUrl: string, apiKey: string) {
|
||||
const client = createClient<paths>({
|
||||
baseUrl: baseUrl.replace(/\/+$/, ""),
|
||||
});
|
||||
|
||||
client.use({
|
||||
async onRequest({ request }) {
|
||||
const headers = new Headers(request.headers);
|
||||
headers.set("X-Emby-Token", apiKey);
|
||||
headers.set(
|
||||
"X-Emby-Authorization",
|
||||
'MediaBrowser Client="jellarr-ts", Device="cli", Version="0.0.1"',
|
||||
);
|
||||
return new Request(request, { headers });
|
||||
},
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"jellarr/src/internal/api"
|
||||
"jellarr/src/internal/apply"
|
||||
"jellarr/src/internal/config"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgPath = "config/config.yml"
|
||||
applyAll = apply.ApplyAll
|
||||
)
|
||||
|
||||
func getConfigFilePath() string {
|
||||
fs := flag.NewFlagSet("jellarr", flag.ContinueOnError)
|
||||
cfg := cfgPath
|
||||
fs.StringVar(&cfg, "configFile", cfgPath, "path to config file")
|
||||
fs.Parse(os.Args[1:])
|
||||
return cfg
|
||||
}
|
||||
|
||||
func run() error {
|
||||
cfg, err := config.Load(getConfigFilePath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
key := os.Getenv("JELLARR_API_KEY")
|
||||
if key == "" {
|
||||
return fmt.Errorf("set JELLARR_API_KEY")
|
||||
}
|
||||
|
||||
jf := api.New(cfg.BaseUrl, key)
|
||||
return applyAll(context.Background(), jf, cfg)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("✅ jellarr apply complete")
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"jellarr/src/internal/api"
|
||||
"jellarr/src/internal/config"
|
||||
)
|
||||
|
||||
func withArgs(args []string, fn func()) {
|
||||
orig := os.Args
|
||||
defer func() { os.Args = orig }()
|
||||
os.Args = args
|
||||
fn()
|
||||
}
|
||||
|
||||
func TestGetConfigWithDefaultFilePath(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
// Act
|
||||
withArgs([]string{"jellarr"}, func() {
|
||||
path := getConfigFilePath()
|
||||
|
||||
// Assert
|
||||
g.Expect(path).To(Equal(cfgPath)) // default is "config/config.yml"
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetConfigWithOverridenFilePath(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
override := "/tmp/test.yml"
|
||||
|
||||
// Act
|
||||
withArgs([]string{"jellarr", "--configFile", override}, func() {
|
||||
path := getConfigFilePath()
|
||||
|
||||
// Assert
|
||||
g.Expect(path).To(Equal(override))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunWithNoApiKey(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
_ = os.Unsetenv("JELLARR_API_KEY")
|
||||
sample := "../../../sample-config.yml"
|
||||
|
||||
var err error
|
||||
|
||||
// Act
|
||||
withArgs([]string{"jellarr", "--configFile", sample}, func() {
|
||||
err = run()
|
||||
})
|
||||
|
||||
// Assert
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring("JELLARR_API_KEY"))
|
||||
}
|
||||
|
||||
func TestRunHappyPath(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
os.Setenv("JELLARR_API_KEY", "dummy")
|
||||
defer os.Unsetenv("JELLARR_API_KEY")
|
||||
|
||||
called := false
|
||||
orig_applyAll := applyAll
|
||||
defer func() { applyAll = orig_applyAll }()
|
||||
|
||||
applyAll = func(_ context.Context, _ api.JF, _ *config.Config) error {
|
||||
called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
sample := "../../../sample-config.yml"
|
||||
var err error
|
||||
|
||||
// Act
|
||||
withArgs([]string{"jellarr", "--configFile", sample}, func() {
|
||||
err = run()
|
||||
})
|
||||
|
||||
// Assert
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(called).To(BeTrue(), "run should invoke applyAll when everything is configured correctly")
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
jellyfin "github.com/sj14/jellyfin-go/api"
|
||||
)
|
||||
|
||||
type configurationAPI interface {
|
||||
GetConfiguration(ctx context.Context) jellyfin.ApiGetConfigurationRequest
|
||||
GetConfigurationExecute(r jellyfin.ApiGetConfigurationRequest) (*jellyfin.ServerConfiguration, *http.Response, error)
|
||||
UpdateConfiguration(ctx context.Context) jellyfin.ApiUpdateConfigurationRequest
|
||||
UpdateConfigurationExecute(r jellyfin.ApiUpdateConfigurationRequest) (*http.Response, error)
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Conf configurationAPI
|
||||
}
|
||||
|
||||
func new(baseURL, apiKey string) JF {
|
||||
cfg := &jellyfin.Configuration{
|
||||
Servers: jellyfin.ServerConfigurations{{URL: baseURL}},
|
||||
DefaultHeader: map[string]string{
|
||||
"Authorization": fmt.Sprintf(`MediaBrowser Token="%s"`, apiKey),
|
||||
},
|
||||
}
|
||||
ac := jellyfin.NewAPIClient(cfg)
|
||||
return &Client{
|
||||
Conf: ac.ConfigurationAPI,
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
jellyfin "github.com/sj14/jellyfin-go/api"
|
||||
|
||||
"jellarr/src/internal/mapper"
|
||||
"jellarr/src/internal/model"
|
||||
)
|
||||
|
||||
func (cl *Client) GetSystem(ctx context.Context) (model.SystemState, error) {
|
||||
req := cl.Conf.GetConfiguration(ctx)
|
||||
cfg, _, err := cl.Conf.GetConfigurationExecute(req)
|
||||
if err != nil {
|
||||
return model.SystemState{}, err
|
||||
}
|
||||
return model.SystemState{
|
||||
EnableMetrics: cfg.GetEnableMetrics(),
|
||||
PluginRepositories: mapper.FromJFRepos(cfg.GetPluginRepositories()),
|
||||
TrickplayOptions: mapper.FromJFTrickplayOptions(cfg.GetTrickplayOptions()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cl *Client) UpdateSystem(ctx context.Context, in model.SystemSpec) error {
|
||||
body := jellyfin.ServerConfiguration{}
|
||||
|
||||
if in.EnableMetrics != nil {
|
||||
body.EnableMetrics = in.EnableMetrics
|
||||
}
|
||||
|
||||
if in.PluginRepositories != nil {
|
||||
body.PluginRepositories = mapper.ToJFRepos(in.PluginRepositories)
|
||||
}
|
||||
|
||||
if in.TrickplayOptions != nil {
|
||||
body.TrickplayOptions = mapper.ToJFTrickplayOptions(*in.TrickplayOptions)
|
||||
}
|
||||
|
||||
req := cl.Conf.UpdateConfiguration(ctx).ServerConfiguration(body)
|
||||
_, err := cl.Conf.UpdateConfigurationExecute(req)
|
||||
return err
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"jellarr/src/internal/model"
|
||||
)
|
||||
|
||||
type JF interface {
|
||||
GetSystem(ctx context.Context) (model.SystemState, error)
|
||||
UpdateSystem(ctx context.Context, in model.SystemSpec) error
|
||||
}
|
||||
|
||||
func New(baseURL, apiKey string) JF { return new(baseURL, apiKey) }
|
||||
@@ -1,12 +0,0 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"jellarr/src/internal/api"
|
||||
"jellarr/src/internal/config"
|
||||
)
|
||||
|
||||
func ApplyAll(ctx context.Context, jf api.JF, cfg *config.Config) error {
|
||||
return ApplySystem(ctx, jf, cfg.System)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"jellarr/src/internal/api"
|
||||
"jellarr/src/internal/model"
|
||||
)
|
||||
|
||||
func ApplySystem(ctx context.Context, jf api.JF, desired model.SystemSpec) error {
|
||||
cur, err := jf.GetSystem(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get system: %w", err)
|
||||
}
|
||||
|
||||
changed := false
|
||||
|
||||
if desired.EnableMetrics != nil && cur.EnableMetrics != *desired.EnableMetrics {
|
||||
changed = true
|
||||
}
|
||||
|
||||
if desired.PluginRepositories != nil && !EqualReposUnordered(cur.PluginRepositories, desired.PluginRepositories) {
|
||||
changed = true
|
||||
}
|
||||
|
||||
if desired.TrickplayOptions != nil && !AreTrickplayOptionsEqual(cur.TrickplayOptions, *desired.TrickplayOptions) {
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
fmt.Println("→ updating system config")
|
||||
if err := jf.UpdateSystem(ctx, desired); err != nil {
|
||||
return fmt.Errorf("update system failed: %w", err)
|
||||
}
|
||||
fmt.Println("✓ updated system config")
|
||||
return nil
|
||||
} else {
|
||||
fmt.Println("✓ system config already up to date")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func EqualReposUnordered(a, b []model.PluginRepository) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
ac := append([]model.PluginRepository(nil), a...)
|
||||
bc := append([]model.PluginRepository(nil), b...)
|
||||
SortRepos(ac)
|
||||
SortRepos(bc)
|
||||
for i := range ac {
|
||||
if ac[i] != bc[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func SortRepos(r []model.PluginRepository) {
|
||||
sort.Slice(r, func(i, j int) bool {
|
||||
if r[i].Name != r[j].Name {
|
||||
return r[i].Name < r[j].Name
|
||||
}
|
||||
if r[i].URL != r[j].URL {
|
||||
return r[i].URL < r[j].URL
|
||||
}
|
||||
if r[i].Enabled != r[j].Enabled {
|
||||
return r[i].Enabled && !r[j].Enabled
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func AreTrickplayOptionsEqual(a, b model.TrickplayOptions) bool {
|
||||
return a.EnableHwAcceleration == b.EnableHwAcceleration
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"jellarr/src/internal/model"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Version int `yaml:"version"`
|
||||
BaseUrl string `yaml:"base_url"`
|
||||
System model.SystemSpec `yaml:"system"`
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c Config
|
||||
if err := yaml.Unmarshal(b, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package mapper
|
||||
|
||||
import (
|
||||
jellyfin "github.com/sj14/jellyfin-go/api"
|
||||
|
||||
"jellarr/src/internal/model"
|
||||
)
|
||||
|
||||
func ToJFRepos(in []model.PluginRepository) []jellyfin.RepositoryInfo {
|
||||
out := make([]jellyfin.RepositoryInfo, 0, len(in))
|
||||
for _, r := range in {
|
||||
name := jellyfin.NewNullableString(&r.Name)
|
||||
url := jellyfin.NewNullableString(&r.URL)
|
||||
enabled := r.Enabled
|
||||
out = append(out, jellyfin.RepositoryInfo{
|
||||
Name: *name,
|
||||
Url: *url,
|
||||
Enabled: &enabled,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func FromJFRepos(in []jellyfin.RepositoryInfo) []model.PluginRepository {
|
||||
out := make([]model.PluginRepository, 0, len(in))
|
||||
for _, r := range in {
|
||||
out = append(out, model.PluginRepository{
|
||||
Name: r.GetName(),
|
||||
URL: r.GetUrl(),
|
||||
Enabled: r.GetEnabled(),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func FromJFTrickplayOptions(in jellyfin.TrickplayOptions) model.TrickplayOptions {
|
||||
return model.TrickplayOptions{
|
||||
EnableHwAcceleration: in.GetEnableHwAcceleration(),
|
||||
}
|
||||
}
|
||||
|
||||
func ToJFTrickplayOptions(in model.TrickplayOptions) *jellyfin.TrickplayOptions {
|
||||
return &jellyfin.TrickplayOptions{
|
||||
EnableHwAcceleration: &in.EnableHwAcceleration,
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package model
|
||||
|
||||
type PluginRepository struct {
|
||||
Name string `yaml:"name"`
|
||||
URL string `yaml:"url"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
}
|
||||
|
||||
type TrickplayOptions struct {
|
||||
EnableHwAcceleration bool `yaml:"enableHwAcceleration"`
|
||||
}
|
||||
|
||||
type SystemSpec struct {
|
||||
EnableMetrics *bool `yaml:"enableMetrics,omitempty"`
|
||||
PluginRepositories []PluginRepository `yaml:"pluginRepositories,omitempty"`
|
||||
TrickplayOptions *TrickplayOptions `yaml:"trickplayOptions,omitempty"`
|
||||
}
|
||||
|
||||
type SystemState struct {
|
||||
EnableMetrics bool
|
||||
PluginRepositories []PluginRepository
|
||||
TrickplayOptions TrickplayOptions
|
||||
}
|
||||
43
src/system.ts
Normal file
43
src/system.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import YAML from "yaml";
|
||||
import { promises as fs } from "fs";
|
||||
import { makeClient } from "./client";
|
||||
|
||||
interface Config {
|
||||
version: number;
|
||||
base_url: string;
|
||||
system: { enableMetrics: boolean };
|
||||
}
|
||||
|
||||
export async function applySystem(path: string) {
|
||||
const cfg = YAML.parse(await fs.readFile(path, "utf8")) as Config;
|
||||
const { base_url, system } = cfg;
|
||||
|
||||
const apiKey = process.env.JELLYFIN_API_KEY;
|
||||
if (!apiKey) throw new Error("JELLYFIN_API_KEY required");
|
||||
|
||||
const jf = makeClient(base_url, apiKey);
|
||||
|
||||
const read = await jf.GET("/System/Configuration");
|
||||
if (read.error) {
|
||||
throw new Error(
|
||||
`Failed to get config: ${read.response?.status ?? "unknown"}`,
|
||||
);
|
||||
}
|
||||
const current = read.data;
|
||||
|
||||
if (current?.EnableMetrics === system.enableMetrics) {
|
||||
console.log("✓ Already up to date");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("→ Updating EnableMetrics...");
|
||||
|
||||
const body = { ...(current as any), EnableMetrics: system.enableMetrics };
|
||||
|
||||
const write = await jf.POST("/System/Configuration", { body });
|
||||
if (write.error) {
|
||||
throw new Error(`Update failed: ${write.response?.status ?? "unknown"}`);
|
||||
}
|
||||
|
||||
console.log("✓ Updated");
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
. "github.com/onsi/gomega/gstruct"
|
||||
|
||||
jellyfin "github.com/sj14/jellyfin-go/api"
|
||||
|
||||
"jellarr/src/internal/api"
|
||||
"jellarr/src/internal/model"
|
||||
)
|
||||
|
||||
type mockClient struct {
|
||||
getCalled bool
|
||||
updateCalled bool
|
||||
err error
|
||||
cfg *jellyfin.ServerConfiguration
|
||||
}
|
||||
|
||||
func (m *mockClient) GetConfiguration(ctx context.Context) jellyfin.ApiGetConfigurationRequest {
|
||||
m.getCalled = true
|
||||
var r jellyfin.ApiGetConfigurationRequest
|
||||
return r
|
||||
}
|
||||
|
||||
func (m *mockClient) GetConfigurationExecute(_ jellyfin.ApiGetConfigurationRequest) (*jellyfin.ServerConfiguration, *http.Response, error) {
|
||||
if m.err != nil {
|
||||
return nil, nil, m.err
|
||||
}
|
||||
if m.cfg == nil {
|
||||
m.cfg = &jellyfin.ServerConfiguration{}
|
||||
}
|
||||
return m.cfg, nil, nil
|
||||
}
|
||||
|
||||
func (m *mockClient) UpdateConfiguration(ctx context.Context) jellyfin.ApiUpdateConfigurationRequest {
|
||||
m.updateCalled = true
|
||||
var r jellyfin.ApiUpdateConfigurationRequest
|
||||
return r
|
||||
}
|
||||
|
||||
func (m *mockClient) UpdateConfigurationExecute(_ jellyfin.ApiUpdateConfigurationRequest) (*http.Response, error) {
|
||||
if m.err != nil {
|
||||
return nil, m.err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T { return &v }
|
||||
|
||||
func TestGetSystemHappyPath(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
m := &mockClient{
|
||||
cfg: &jellyfin.ServerConfiguration{
|
||||
EnableMetrics: ptr(true),
|
||||
PluginRepositories: []jellyfin.RepositoryInfo{
|
||||
{
|
||||
Name: *jellyfin.NewNullableString(ptr("Repo")),
|
||||
Url: *jellyfin.NewNullableString(ptr("https://repo")),
|
||||
Enabled: ptr(true),
|
||||
},
|
||||
},
|
||||
TrickplayOptions: &jellyfin.TrickplayOptions{
|
||||
EnableHwAcceleration: ptr(true),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Act
|
||||
cl := &api.Client{Conf: m}
|
||||
state, err := cl.GetSystem(context.Background())
|
||||
|
||||
// Assert
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(m.getCalled).To(BeTrue(), "GetConfiguration should be called")
|
||||
g.Expect(state).To(
|
||||
MatchAllFields(Fields{
|
||||
"EnableMetrics": BeTrue(),
|
||||
"PluginRepositories": ConsistOf(
|
||||
MatchAllFields(Fields{
|
||||
"Name": Equal("Repo"),
|
||||
"URL": Equal("https://repo"),
|
||||
"Enabled": BeTrue(),
|
||||
}),
|
||||
),
|
||||
"TrickplayOptions": MatchAllFields(Fields{
|
||||
"EnableHwAcceleration": BeTrue(),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func WhenGetConfigurationErrorsThenGetSystemErrors(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
m := &mockClient{err: errors.New("boom")}
|
||||
|
||||
// Act
|
||||
cl := &api.Client{Conf: m}
|
||||
_, err := cl.GetSystem(context.Background())
|
||||
|
||||
// Assert
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring("boom"))
|
||||
g.Expect(m.getCalled).To(BeTrue())
|
||||
}
|
||||
|
||||
func TestUpdateSystemHappyPath(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
m := &mockClient{}
|
||||
spec := model.SystemSpec{
|
||||
EnableMetrics: ptr(true),
|
||||
PluginRepositories: []model.PluginRepository{
|
||||
{Name: "A", URL: "https://repo", Enabled: true},
|
||||
},
|
||||
TrickplayOptions: &model.TrickplayOptions{
|
||||
EnableHwAcceleration: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Act
|
||||
cl := &api.Client{Conf: m}
|
||||
err := cl.UpdateSystem(context.Background(), spec)
|
||||
|
||||
// Assert
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(m.updateCalled).To(BeTrue())
|
||||
}
|
||||
|
||||
func WhenUpdateConfigurationErrorsThenUpdateSystemErrors(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
m := &mockClient{err: errors.New("fail")}
|
||||
spec := model.SystemSpec{
|
||||
EnableMetrics: ptr(true),
|
||||
PluginRepositories: []model.PluginRepository{
|
||||
{Name: "A", URL: "https://repo", Enabled: true},
|
||||
},
|
||||
TrickplayOptions: &model.TrickplayOptions{
|
||||
EnableHwAcceleration: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Act
|
||||
cl := &api.Client{Conf: m}
|
||||
|
||||
// Assert
|
||||
err := cl.UpdateSystem(context.Background(), spec)
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring("fail"))
|
||||
g.Expect(m.updateCalled).To(BeTrue())
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package apply_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"jellarr/src/internal/apply"
|
||||
"jellarr/src/internal/model"
|
||||
)
|
||||
|
||||
type mockJF struct {
|
||||
state model.SystemState
|
||||
updatedSpec *model.SystemSpec
|
||||
}
|
||||
|
||||
func (m *mockJF) GetSystem(ctx context.Context) (model.SystemState, error) { return m.state, nil }
|
||||
func (m *mockJF) UpdateSystem(ctx context.Context, in model.SystemSpec) error {
|
||||
m.updatedSpec = &in
|
||||
return nil
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T { return &v }
|
||||
|
||||
func WhenNoChangesThenApplySystemDoesNotApplyAnyChanges(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
m := &mockJF{
|
||||
state: model.SystemState{
|
||||
EnableMetrics: true,
|
||||
PluginRepositories: []model.PluginRepository{
|
||||
{Name: "A", URL: "u1", Enabled: true},
|
||||
},
|
||||
TrickplayOptions: model.TrickplayOptions{
|
||||
EnableHwAcceleration: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
desired := model.SystemSpec{
|
||||
EnableMetrics: ptr(true),
|
||||
PluginRepositories: []model.PluginRepository{
|
||||
{Name: "A", URL: "u1", Enabled: true},
|
||||
},
|
||||
TrickplayOptions: &model.TrickplayOptions{
|
||||
EnableHwAcceleration: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Act
|
||||
err := apply.ApplySystem(context.Background(), m, desired)
|
||||
|
||||
// Assert
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(m.updatedSpec).To(BeNil(), "should not update when desired == current")
|
||||
}
|
||||
|
||||
func WhenChangesThenApplySystemAppliesChanges(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
m := &mockJF{
|
||||
state: model.SystemState{
|
||||
EnableMetrics: true,
|
||||
PluginRepositories: []model.PluginRepository{
|
||||
{Name: "A", URL: "u1", Enabled: true},
|
||||
},
|
||||
TrickplayOptions: model.TrickplayOptions{
|
||||
EnableHwAcceleration: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
desired := model.SystemSpec{
|
||||
EnableMetrics: ptr(true),
|
||||
PluginRepositories: []model.PluginRepository{
|
||||
{Name: "A", URL: "u1", Enabled: true},
|
||||
{Name: "B", URL: "u2", Enabled: false},
|
||||
},
|
||||
TrickplayOptions: &model.TrickplayOptions{
|
||||
EnableHwAcceleration: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Act
|
||||
err := apply.ApplySystem(context.Background(), m, desired)
|
||||
|
||||
// Assert
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(m.updatedSpec).NotTo(BeNil(), "expected update to be triggered")
|
||||
g.Expect(*m.updatedSpec).To(Equal(desired))
|
||||
}
|
||||
|
||||
func TestEqualReposUnordered(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
a := []model.PluginRepository{
|
||||
{Name: "A", URL: "u1", Enabled: true},
|
||||
{Name: "B", URL: "u2", Enabled: false},
|
||||
}
|
||||
b := []model.PluginRepository{
|
||||
{Name: "B", URL: "u2", Enabled: false},
|
||||
{Name: "A", URL: "u1", Enabled: true},
|
||||
}
|
||||
c := []model.PluginRepository{
|
||||
{Name: "A", URL: "u1", Enabled: false},
|
||||
}
|
||||
|
||||
// Act & Assert
|
||||
g.Expect(apply.EqualReposUnordered(a, b)).To(BeTrue(), "same sets should be equal regardless of order")
|
||||
g.Expect(apply.EqualReposUnordered(a, c)).To(BeFalse(), "different sets should not be equal")
|
||||
}
|
||||
|
||||
func TestAreTrickplayOptionsEqual(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
// Act & Assert
|
||||
g.Expect(apply.AreTrickplayOptionsEqual(
|
||||
model.TrickplayOptions{EnableHwAcceleration: true},
|
||||
model.TrickplayOptions{EnableHwAcceleration: true},
|
||||
)).To(BeTrue())
|
||||
|
||||
g.Expect(apply.AreTrickplayOptionsEqual(
|
||||
model.TrickplayOptions{EnableHwAcceleration: true},
|
||||
model.TrickplayOptions{EnableHwAcceleration: false},
|
||||
)).To(BeFalse())
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
. "github.com/onsi/gomega/gstruct"
|
||||
|
||||
"jellarr/src/internal/config"
|
||||
)
|
||||
|
||||
func TestLoadHappyPath(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
p := writeTmpYml(t, `
|
||||
version: 1
|
||||
base_url: "http://localhost:8096"
|
||||
system:
|
||||
enableMetrics: true
|
||||
pluginRepositories:
|
||||
- name: "Repo"
|
||||
url: "https://repo"
|
||||
enabled: true
|
||||
trickplayOptions:
|
||||
enableHwAcceleration: true
|
||||
`)
|
||||
// Act
|
||||
cfg, err := config.Load(p)
|
||||
|
||||
// Assert
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
g.Expect(*cfg).To(MatchFields(IgnoreExtras, Fields{
|
||||
"Version": Equal(1),
|
||||
"BaseUrl": Equal("http://localhost:8096"),
|
||||
"System": MatchAllFields(Fields{
|
||||
"EnableMetrics": PointTo(BeTrue()),
|
||||
"PluginRepositories": ConsistOf(
|
||||
MatchAllFields(Fields{
|
||||
"Name": Equal("Repo"),
|
||||
"URL": Equal("https://repo"),
|
||||
"Enabled": BeTrue(),
|
||||
}),
|
||||
),
|
||||
"TrickplayOptions": PointTo(MatchAllFields(Fields{
|
||||
"EnableHwAcceleration": BeTrue(),
|
||||
})),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
func WhenNoEnableMetricsInConfigThenEnableMetricsInSystemSpecIsNil(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
p := writeTmpYml(t, `
|
||||
version: 1
|
||||
base_url: "http://localhost:8096"
|
||||
system:
|
||||
pluginRepositories:
|
||||
- name: "Repo"
|
||||
url: "https://repo"
|
||||
enabled: true
|
||||
trickplayOptions:
|
||||
enableHwAcceleration: true
|
||||
`)
|
||||
|
||||
// Act
|
||||
cfg, err := config.Load(p)
|
||||
|
||||
// Assert
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
g.Expect(cfg.System.EnableMetrics).To(BeNil())
|
||||
g.Expect(cfg.System.PluginRepositories).NotTo(BeNil())
|
||||
g.Expect(cfg.System.TrickplayOptions).NotTo(BeNil())
|
||||
}
|
||||
|
||||
func WhenNoPluginRepositoriesInConfigThenPluginRepositoriesInSystemSpecIsNil(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
p := writeTmpYml(t, `
|
||||
version: 1
|
||||
base_url: "http://localhost:8096"
|
||||
system:
|
||||
enableMetrics: true
|
||||
trickplayOptions:
|
||||
enableHwAcceleration: true
|
||||
`)
|
||||
|
||||
// Act
|
||||
cfg, err := config.Load(p)
|
||||
|
||||
// Assert
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
g.Expect(cfg.System.EnableMetrics).NotTo(BeNil())
|
||||
g.Expect(cfg.System.PluginRepositories).To(BeNil())
|
||||
g.Expect(cfg.System.TrickplayOptions).NotTo(BeNil())
|
||||
}
|
||||
|
||||
func WhenNoTrickplayOptionsInConfigThenTrickplayOptionsInSystemSpecIsNil(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
p := writeTmpYml(t, `
|
||||
version: 1
|
||||
base_url: "http://localhost:8096"
|
||||
system:
|
||||
enableMetrics: true
|
||||
pluginRepositories:
|
||||
- name: "Repo"
|
||||
url: "https://repo"
|
||||
enabled: true
|
||||
`)
|
||||
|
||||
// Act
|
||||
cfg, err := config.Load(p)
|
||||
|
||||
// Assert
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
g.Expect(cfg.System.EnableMetrics).NotTo(BeNil())
|
||||
g.Expect(cfg.System.PluginRepositories).NotTo(BeNil())
|
||||
g.Expect(cfg.System.TrickplayOptions).To(BeNil())
|
||||
}
|
||||
|
||||
func WhenBadYAMLThenLoadErrors(t *testing.T) {
|
||||
//Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
p := writeTmpYml(t, "system: [1,2")
|
||||
|
||||
// Act
|
||||
_, err := config.Load(p)
|
||||
|
||||
// Assert
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
}
|
||||
|
||||
func writeTmpYml(t *testing.T, yml string) string {
|
||||
dir := t.TempDir()
|
||||
p := filepath.Join(dir, "cfg.yml")
|
||||
if err := os.WriteFile(p, []byte(yml), 0o644); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package mapper_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
. "github.com/onsi/gomega/gstruct"
|
||||
|
||||
jellyfin "github.com/sj14/jellyfin-go/api"
|
||||
|
||||
"jellarr/src/internal/mapper"
|
||||
"jellarr/src/internal/model"
|
||||
)
|
||||
|
||||
func TestRepoRoundTripHappyPath(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
in := []model.PluginRepository{
|
||||
{Name: "A", URL: "u1", Enabled: true},
|
||||
{Name: "B", URL: "u2", Enabled: false},
|
||||
}
|
||||
|
||||
// Act
|
||||
jf := mapper.ToJFRepos(in)
|
||||
back := mapper.FromJFRepos(jf)
|
||||
|
||||
// Assert
|
||||
g.Expect(back).To(Equal(in))
|
||||
}
|
||||
|
||||
func TestRepoRoundTripWithEmptyValues(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
in := []model.PluginRepository{}
|
||||
|
||||
// Act
|
||||
jf := mapper.ToJFRepos(in)
|
||||
back := mapper.FromJFRepos(jf)
|
||||
|
||||
// Assert
|
||||
g.Expect(back).To(Equal(in))
|
||||
}
|
||||
|
||||
func TestFromJFReposHandlesNull(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
jf := []jellyfin.RepositoryInfo{
|
||||
{},
|
||||
}
|
||||
|
||||
// Act
|
||||
back := mapper.FromJFRepos(jf)
|
||||
|
||||
// Assert
|
||||
g.Expect(back).To(Equal([]model.PluginRepository{
|
||||
{Name: "", URL: "", Enabled: false},
|
||||
}))
|
||||
}
|
||||
|
||||
func TestToJFReposHandlesEmptyValues(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
in := []model.PluginRepository{
|
||||
{Name: "", URL: "", Enabled: false},
|
||||
}
|
||||
|
||||
// Act
|
||||
out := mapper.ToJFRepos(in)
|
||||
|
||||
// Assert
|
||||
g.Expect(out).To(HaveLen(1))
|
||||
|
||||
g.Expect(out[0]).To(MatchAllFields(Fields{
|
||||
"Name": WithTransform(func(n jellyfin.NullableString) string {
|
||||
if v := n.Get(); v != nil {
|
||||
return *v
|
||||
}
|
||||
return ""
|
||||
}, Equal("")),
|
||||
|
||||
"Url": WithTransform(func(u jellyfin.NullableString) string {
|
||||
if v := u.Get(); v != nil {
|
||||
return *v
|
||||
}
|
||||
return ""
|
||||
}, Equal("")),
|
||||
|
||||
"Enabled": PointTo(BeFalse()),
|
||||
}))
|
||||
}
|
||||
|
||||
func TestTrickplayOptionsRoundTripHappyPath(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
in := model.TrickplayOptions{
|
||||
EnableHwAcceleration: true,
|
||||
}
|
||||
|
||||
// Act
|
||||
jf := mapper.ToJFTrickplayOptions(in)
|
||||
back := mapper.FromJFTrickplayOptions(*jf)
|
||||
|
||||
// Assert
|
||||
g.Expect(back).To(Equal(in))
|
||||
}
|
||||
|
||||
func TestTrickplayOptionsRoundTripWithEmptyValues(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
|
||||
in := model.TrickplayOptions{}
|
||||
|
||||
// Act
|
||||
jf := mapper.ToJFTrickplayOptions(in)
|
||||
back := mapper.FromJFTrickplayOptions(*jf)
|
||||
|
||||
// Assert
|
||||
g.Expect(back).To(Equal(in))
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package model_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
. "github.com/onsi/gomega/gstruct"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"jellarr/src/internal/model"
|
||||
)
|
||||
|
||||
func TestSystemSpecYAML(t *testing.T) {
|
||||
// Arrange
|
||||
g := NewWithT(t)
|
||||
data, err := os.ReadFile("../../../../sample-config.yml")
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
var root struct {
|
||||
Version int `yaml:"version"`
|
||||
BaseURL string `yaml:"base_url"`
|
||||
System model.SystemSpec `yaml:"system"`
|
||||
}
|
||||
|
||||
// Act
|
||||
err = yaml.Unmarshal(data, &root)
|
||||
|
||||
// Assert
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(root.System.EnableMetrics).To(PointTo(BeTrue()))
|
||||
g.Expect(root.System.PluginRepositories).To(ConsistOf(
|
||||
MatchAllFields(Fields{
|
||||
"Name": Equal("Jellyfin Official"),
|
||||
"URL": Equal("https://repo.jellyfin.org/releases/plugin/manifest.json"),
|
||||
"Enabled": BeTrue(),
|
||||
}),
|
||||
))
|
||||
}
|
||||
@@ -17,13 +17,17 @@ _: {
|
||||
priority = 30;
|
||||
};
|
||||
|
||||
gofmt = {
|
||||
prettier = {
|
||||
enable = true;
|
||||
priority = 40;
|
||||
settings.editorconfig = true;
|
||||
};
|
||||
|
||||
deno = {
|
||||
enable = true;
|
||||
includes = [
|
||||
"*.md"
|
||||
];
|
||||
priority = 50;
|
||||
};
|
||||
};
|
||||
|
||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src", "generated"]
|
||||
}
|
||||
Reference in New Issue
Block a user