mirror of
https://github.com/navidrome/navidrome.git
synced 2026-01-06 05:48:09 -05:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47311d16cf | ||
|
|
ef3466787d | ||
|
|
b7fd116bd8 | ||
|
|
34ad740e07 | ||
|
|
79454d7a92 | ||
|
|
87cc397bc3 | ||
|
|
37602a2049 | ||
|
|
56ea380bb3 | ||
|
|
177ace1cee | ||
|
|
61e3fe21ff | ||
|
|
8dcca76ec9 | ||
|
|
1dd3a794f8 | ||
|
|
6c5dd245fe | ||
|
|
3b3ad65612 | ||
|
|
e6f798811d | ||
|
|
371e8ab6ca | ||
|
|
69c19e946c | ||
|
|
d7edbf93f0 | ||
|
|
fb4d920fba | ||
|
|
5a072fbd10 | ||
|
|
79c9d8f4f4 | ||
|
|
871bf5a70a | ||
|
|
e4af235ce9 | ||
|
|
00384a60f3 | ||
|
|
f7b3ff4b34 | ||
|
|
eaa48306fc | ||
|
|
f5572b8447 | ||
|
|
a756751cc6 | ||
|
|
b8a3af090d | ||
|
|
d534cb96a9 | ||
|
|
f1e1d3bc07 | ||
|
|
694be54428 | ||
|
|
76531fb1cd | ||
|
|
716f4c5cf7 | ||
|
|
ba2d4b6859 | ||
|
|
2ec5e47328 | ||
|
|
b3f70538a9 | ||
|
|
de115ff466 | ||
|
|
129f02b36b | ||
|
|
1a8d219197 | ||
|
|
80c8d85cb9 | ||
|
|
db02f5f07f | ||
|
|
579294b0f1 | ||
|
|
f83d0d471d | ||
|
|
3b7d7bdb04 | ||
|
|
05958f5195 | ||
|
|
6cf4b81de9 | ||
|
|
689449df9e | ||
|
|
dae938de6f | ||
|
|
f6617ff77d | ||
|
|
defdc2ea6b | ||
|
|
1fd6571a87 | ||
|
|
4c0250f9f8 | ||
|
|
0e1735e7a9 | ||
|
|
a698e434fd | ||
|
|
95f658336c | ||
|
|
69dc4d97b3 | ||
|
|
4aeb63c16e | ||
|
|
e5efadf99e | ||
|
|
d117d5794d | ||
|
|
d09a2182e0 | ||
|
|
b8b09820b1 | ||
|
|
2cfd7babb3 | ||
|
|
161a9b340c | ||
|
|
605253446a | ||
|
|
f8d9b1508e | ||
|
|
3c4de3c8b5 | ||
|
|
a6c9bf1b15 | ||
|
|
bf6ec67528 | ||
|
|
289ba68824 | ||
|
|
2dfe01963a | ||
|
|
5ed1d5c19f |
@@ -1,6 +1,5 @@
|
||||
.DS_Store
|
||||
ui/node_modules
|
||||
ui/build
|
||||
Jamstash-master
|
||||
Dockerfile
|
||||
docker-compose*.yml
|
||||
@@ -11,4 +10,4 @@ navidrome
|
||||
navidrome.db
|
||||
navidrome.toml
|
||||
assets/*gen.go
|
||||
dist
|
||||
|
||||
|
||||
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
||||
# Upgrade Prettier to 2.0.4. Reformatted all JS files
|
||||
b3f70538a9138bc279a451f4f358605097210d41
|
||||
53
.github/workflows/build.yml
vendored
53
.github/workflows/build.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
go:
|
||||
name: Test Server on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
# TODO Fix tests in Windows
|
||||
# os: [macOS-latest, ubuntu-latest, windows-latest]
|
||||
os: [macOS-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Set up Go 1.14
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Test
|
||||
run: go test -cover ./... -v
|
||||
|
||||
js:
|
||||
name: Test UI
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 13
|
||||
|
||||
- name: npm install dependencies
|
||||
run: |
|
||||
cd ui
|
||||
npm ci
|
||||
|
||||
# TODO: Enable when there are tests to run
|
||||
# - name: npm test
|
||||
# run: |
|
||||
# cd ui
|
||||
# CI=test npm test
|
||||
|
||||
- name: npm build
|
||||
run: |
|
||||
cd ui
|
||||
npm run build
|
||||
17
.github/workflows/docker-tags.sh
vendored
Executable file
17
.github/workflows/docker-tags.sh
vendored
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
GIT_TAG="${GITHUB_REF##refs/tags/}"
|
||||
GIT_BRANCH="${GITHUB_REF##refs/heads/}"
|
||||
GIT_SHA=$(git rev-parse --short HEAD)
|
||||
|
||||
DOCKER_IMAGE_TAG="--tag ${DOCKER_IMAGE}:sha-${GIT_SHA}"
|
||||
|
||||
if [[ $GITHUB_REF != $GIT_TAG ]]; then
|
||||
DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG} --tag ${DOCKER_IMAGE}:${GIT_TAG#v} --tag ${DOCKER_IMAGE}:latest"
|
||||
elif [[ $GITHUB_REF == "refs/heads/master" ]]; then
|
||||
DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG} --tag ${DOCKER_IMAGE}:develop"
|
||||
elif [[ $GIT_BRANCH = feature/* ]]; then
|
||||
DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG} --tag ${DOCKER_IMAGE}:$(echo $GIT_BRANCH | tr / -)"
|
||||
fi
|
||||
|
||||
echo ${DOCKER_IMAGE_TAG}
|
||||
40
.github/workflows/pipeline.dockerfile
vendored
Normal file
40
.github/workflows/pipeline.dockerfile
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
#####################################################
|
||||
### Copy platform specific binary
|
||||
FROM bash as copy-binary
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
RUN echo "Target Platform = ${TARGETPLATFORM}"
|
||||
|
||||
COPY dist .
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then cp navidrome_linux_musl_amd64_linux_amd64/navidrome /navidrome; fi
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then cp navidrome_linux_arm64_linux_arm64/navidrome /navidrome; fi
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then cp navidrome_linux_arm_linux_arm_6/navidrome /navidrome; fi
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then cp navidrome_linux_arm_linux_arm_7/navidrome /navidrome; fi
|
||||
RUN chmod +x /navidrome
|
||||
|
||||
|
||||
#####################################################
|
||||
### Build Final Image
|
||||
FROM alpine as release
|
||||
LABEL maintainer="deluan@navidrome.org"
|
||||
|
||||
# Install ffmpeg and output build config
|
||||
RUN apk add --no-cache ffmpeg
|
||||
RUN ffmpeg -buildconf
|
||||
|
||||
COPY --from=copy-binary /navidrome /app/
|
||||
|
||||
VOLUME ["/data", "/music"]
|
||||
ENV ND_MUSICFOLDER /music
|
||||
ENV ND_DATAFOLDER /data
|
||||
ENV ND_SCANINTERVAL 1m
|
||||
ENV ND_TRANSCODINGCACHESIZE 100MB
|
||||
ENV ND_SESSIONTIMEOUT 30m
|
||||
ENV ND_LOGLEVEL info
|
||||
ENV ND_PORT 4533
|
||||
|
||||
EXPOSE ${ND_PORT}
|
||||
HEALTHCHECK CMD wget -O- http://localhost:${ND_PORT}/ping || exit 1
|
||||
WORKDIR /app
|
||||
|
||||
ENTRYPOINT ["/app/navidrome"]
|
||||
138
.github/workflows/pipeline.yml
vendored
Normal file
138
.github/workflows/pipeline.yml
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
name: Pipeline
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
go:
|
||||
name: Test Server on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
# TODO Fix tests in Windows
|
||||
# os: [macOS-latest, ubuntu-latest, windows-latest]
|
||||
os: [macOS-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Set up Go 1.14
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/cache@v1
|
||||
id: cache-go
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Download dependencies
|
||||
if: steps.cache-go.outputs.cache-hit != 'true'
|
||||
run: go mod download
|
||||
|
||||
- name: Test
|
||||
run: go test -cover ./... -v
|
||||
js:
|
||||
name: Build JS bundle
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 13
|
||||
|
||||
- uses: actions/cache@v1
|
||||
id: cache-npm
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('ui/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: npm install dependencies
|
||||
run: |
|
||||
cd ui
|
||||
npm ci
|
||||
|
||||
- name: npm build
|
||||
run: |
|
||||
cd ui
|
||||
npm run build
|
||||
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: js-bundle
|
||||
path: ui/build
|
||||
|
||||
binaries:
|
||||
name: Binaries
|
||||
needs: [js, go]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Unshallow
|
||||
run: git fetch --prune --unshallow
|
||||
|
||||
- uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: js-bundle
|
||||
path: ui/build
|
||||
|
||||
- name: Run GoReleaser - SNAPSHOT
|
||||
if: startsWith(github.ref, 'refs/tags/') != true
|
||||
uses: docker://deluan/ci-goreleaser:1.14.1-1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: goreleaser release --rm-dist --skip-publish --snapshot
|
||||
|
||||
- name: Run GoReleaser - RELEASE
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: docker://deluan/ci-goreleaser:1.14.1-1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: goreleaser release --rm-dist
|
||||
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: binaries
|
||||
path: dist
|
||||
|
||||
docker:
|
||||
name: Docker images
|
||||
needs: [binaries]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: binaries
|
||||
path: dist
|
||||
|
||||
- name: Build the Docker image and push
|
||||
env:
|
||||
DOCKER_IMAGE: ${{secrets.DOCKER_IMAGE}}
|
||||
DOCKER_PLATFORM: linux/amd64,linux/arm/v7,linux/arm64
|
||||
run: |
|
||||
echo ${{secrets.DOCKER_PASSWORD}} | docker login -u ${{secrets.DOCKER_USERNAME}} --password-stdin
|
||||
docker buildx build --platform ${DOCKER_PLATFORM} `.github/workflows/docker-tags.sh` -f .github/workflows/pipeline.dockerfile --push .
|
||||
31
.github/workflows/release.yml
vendored
31
.github/workflows/release.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 13.12
|
||||
- name: Build UI
|
||||
run: |
|
||||
cd ui
|
||||
npm ci
|
||||
npm run build
|
||||
- name: Fetch tags
|
||||
run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
||||
- name: Run GoReleaser
|
||||
uses: docker://bepsays/ci-goreleaser:1.14-1
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
args: goreleaser release --rm-dist
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -9,7 +9,6 @@ vendor/*/
|
||||
wiki
|
||||
TODO.md
|
||||
var
|
||||
Artwork
|
||||
navidrome.toml
|
||||
master.zip
|
||||
Jamstash-master
|
||||
@@ -20,3 +19,7 @@ navidrome.db
|
||||
dist
|
||||
music
|
||||
docker-compose.override.yml
|
||||
navidrome.db-shm
|
||||
navidrome.db-wal
|
||||
tags
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
# GoReleaser config
|
||||
project_name: navidrome
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- apt-get update
|
||||
- apt-get install -y gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu
|
||||
- go get -u github.com/go-bindata/go-bindata/...
|
||||
- go-bindata -fs -prefix ui/build -tags embed -nocompress -pkg assets -o assets/embedded_gen.go ui/build/...
|
||||
- git checkout .
|
||||
|
||||
builds:
|
||||
- id: navidrome_darwin
|
||||
@@ -21,7 +18,7 @@ builds:
|
||||
flags:
|
||||
- -tags=embed
|
||||
ldflags:
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
|
||||
|
||||
- id: navidrome_linux_amd64
|
||||
env:
|
||||
@@ -34,7 +31,21 @@ builds:
|
||||
- -tags=embed
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
|
||||
|
||||
- id: navidrome_linux_musl_amd64
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=musl-gcc
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
flags:
|
||||
- -tags=embed
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
|
||||
|
||||
- id: navidrome_linux_arm
|
||||
env:
|
||||
@@ -51,8 +62,7 @@ builds:
|
||||
- -tags=embed
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- "-extld=$CC"
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
|
||||
|
||||
- id: navidrome_linux_arm64
|
||||
env:
|
||||
@@ -66,7 +76,7 @@ builds:
|
||||
- -tags=embed
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
|
||||
|
||||
- id: navidrome_windows_i686
|
||||
env:
|
||||
@@ -81,7 +91,7 @@ builds:
|
||||
- -tags=embed
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
|
||||
|
||||
- id: navidrome_windows_x64
|
||||
env:
|
||||
@@ -96,10 +106,24 @@ builds:
|
||||
- -tags=embed
|
||||
ldflags:
|
||||
- "-extldflags '-static'"
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Tag}}
|
||||
- -X github.com/deluan/navidrome/consts.gitSha={{.ShortCommit}} -X github.com/deluan/navidrome/consts.gitTag={{.Version}}
|
||||
|
||||
archives:
|
||||
-
|
||||
- id: musl
|
||||
builds:
|
||||
- navidrome_linux_musl_amd64
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_musl_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
replacements:
|
||||
linux: Linux
|
||||
amd64: x86_64
|
||||
- id: default
|
||||
builds:
|
||||
- navidrome_darwin
|
||||
- navidrome_linux_amd64
|
||||
- navidrome_linux_arm
|
||||
- navidrome_linux_arm64
|
||||
- navidrome_windows_i686
|
||||
- navidrome_windows_x64
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
@@ -111,16 +135,16 @@ archives:
|
||||
amd64: x86_64
|
||||
|
||||
checksum:
|
||||
name_template: '{{ .ProjectName }}_checksums.txt'
|
||||
name_template: "{{ .ProjectName }}_checksums.txt"
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
name_template: "{{ .Tag }}-SNAPSHOT"
|
||||
|
||||
release:
|
||||
draft: true
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
# sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- "^docs:"
|
||||
|
||||
21
Dockerfile
21
Dockerfile
@@ -1,6 +1,6 @@
|
||||
#####################################################
|
||||
### Build UI bundles
|
||||
FROM node:13.12-alpine AS jsbuilder
|
||||
FROM node:13-alpine AS jsbuilder
|
||||
WORKDIR /src
|
||||
COPY ui/package.json ui/package-lock.json ./
|
||||
RUN npm ci
|
||||
@@ -17,11 +17,6 @@ RUN mkdir -p /src/ui/build
|
||||
RUN apk add -U --no-cache build-base git
|
||||
RUN go get -u github.com/go-bindata/go-bindata/...
|
||||
|
||||
# Download and unpack static ffmpeg
|
||||
ARG FFMPEG_URL=https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
|
||||
RUN wget -O /tmp/ffmpeg.tar.xz ${FFMPEG_URL}
|
||||
RUN cd /tmp && tar xJf ffmpeg.tar.xz && rm ffmpeg.tar.xz
|
||||
|
||||
# Download project dependencies
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
@@ -46,17 +41,12 @@ RUN GIT_TAG=$(git describe --tags `git rev-list --tags --max-count=1`) && \
|
||||
#####################################################
|
||||
### Build Final Image
|
||||
FROM alpine as release
|
||||
MAINTAINER Deluan Quintao <navidrome@deluan.com>
|
||||
|
||||
# Download Tini
|
||||
ENV TINI_VERSION v0.18.0
|
||||
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static /tini
|
||||
RUN chmod +x /tini
|
||||
LABEL maintainer="deluan@navidrome.org"
|
||||
|
||||
COPY --from=gobuilder /src/navidrome /app/
|
||||
COPY --from=gobuilder /tmp/ffmpeg*/ffmpeg /usr/bin/
|
||||
|
||||
# Check if ffmpeg runs properly
|
||||
# Install ffmpeg and output build config
|
||||
RUN apk add --no-cache ffmpeg
|
||||
RUN ffmpeg -buildconf
|
||||
|
||||
VOLUME ["/data", "/music"]
|
||||
@@ -72,5 +62,4 @@ EXPOSE ${ND_PORT}
|
||||
HEALTHCHECK CMD wget -O- http://localhost:${ND_PORT}/ping || exit 1
|
||||
WORKDIR /app
|
||||
|
||||
ENTRYPOINT ["/tini", "--"]
|
||||
CMD ["/app/navidrome"]
|
||||
ENTRYPOINT ["/app/navidrome"]
|
||||
|
||||
9
Makefile
9
Makefile
@@ -2,6 +2,7 @@ GO_VERSION=$(shell grep -e "^go " go.mod | cut -f 2 -d ' ')
|
||||
NODE_VERSION=$(shell cat .nvmrc)
|
||||
|
||||
GIT_SHA=$(shell git rev-parse --short HEAD)
|
||||
GIT_TAG=$(shell git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
## Default target just build the Go project.
|
||||
default:
|
||||
@@ -33,7 +34,7 @@ testall: check_go_env test
|
||||
@(cd ./ui && npm test -- --watchAll=false)
|
||||
.PHONY: testall
|
||||
|
||||
setup: Jamstash-master
|
||||
setup:
|
||||
@which wire || (echo "Installing Wire" && GO111MODULE=off go get -u github.com/google/wire/cmd/wire)
|
||||
@which go-bindata || (echo "Installing BinData" && GO111MODULE=off go get -u github.com/go-bindata/go-bindata/...)
|
||||
@which reflex || (echo "Installing Reflex" && GO111MODULE=off go get -u github.com/cespare/reflex)
|
||||
@@ -76,13 +77,13 @@ check_node_env:
|
||||
.PHONY: check_node_env
|
||||
|
||||
build: check_go_env
|
||||
go build -ldflags="-X github.com/deluan/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/deluan/navidrome/consts.gitTag=master"
|
||||
go build -ldflags="-X github.com/deluan/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/deluan/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT"
|
||||
.PHONY: build
|
||||
|
||||
buildall: check_env
|
||||
@(cd ./ui && npm run build)
|
||||
go-bindata -fs -prefix "ui/build" -tags embed -nocompress -pkg assets -o assets/embedded_gen.go ui/build/...
|
||||
go build -ldflags="-X github.com/deluan/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/deluan/navidrome/consts.gitTag=master" -tags=embed
|
||||
go build -ldflags="-X github.com/deluan/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/deluan/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT" -tags=embed
|
||||
.PHONY: buildall
|
||||
|
||||
release:
|
||||
@@ -95,5 +96,5 @@ release:
|
||||
.PHONY: release
|
||||
|
||||
snapshot:
|
||||
docker run -it -v $(PWD):/workspace -w /workspace bepsays/ci-goreleaser:1.14-1 goreleaser release --rm-dist --skip-publish --snapshot
|
||||
docker run -it -v $(PWD):/workspace -w /workspace deluan/ci-goreleaser:1.14.1-1 goreleaser release --rm-dist --skip-publish --snapshot
|
||||
.PHONY: snapshot
|
||||
|
||||
@@ -110,7 +110,7 @@ To get the cutting-edge, latest version from master, use the image `deluan/navid
|
||||
|
||||
### Build from source
|
||||
|
||||
You will need to install [Go 1.14](https://golang.org/dl/) and [Node 13.12.0](http://nodejs.org).
|
||||
You will need to install [Go 1.14](https://golang.org/dl/) and [Node 13](http://nodejs.org).
|
||||
You'll also need [ffmpeg](https://ffmpeg.org) installed in your system. The setup is very strict, and
|
||||
the steps bellow only work with these specific versions (enforced in the Makefile)
|
||||
|
||||
|
||||
14
bin/fmt.sh
14
bin/fmt.sh
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
gofmtcmd=`which goimports || echo "gofmt"`
|
||||
|
||||
gofiles=$(git diff --name-only --diff-filter=ACM | grep '.go$')
|
||||
[ -z "$gofiles" ] && exit 0
|
||||
|
||||
unformatted=`$gofmtcmd -l $gofiles`
|
||||
[ -z "$unformatted" ] && exit 0
|
||||
|
||||
for f in $unformatted; do
|
||||
$gofmtcmd -w -l "$f"
|
||||
gofmt -s -w -l "$f"
|
||||
done
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Copyright 2012 The Go Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
# git gofmt pre-commit hook
|
||||
#
|
||||
# To use, store as .git/hooks/pre-commit inside your repository and make sure
|
||||
# it has execute permissions.
|
||||
#
|
||||
# This script does not handle file names that contain spaces.
|
||||
|
||||
gofmtcmd=`which goimports || echo "gofmt"`
|
||||
|
||||
gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '.go$')
|
||||
[ -z "$gofiles" ] && exit 0
|
||||
|
||||
unformatted=$($gofmtcmd -l $gofiles)
|
||||
[ -z "$unformatted" ] && exit 0
|
||||
|
||||
# Some files are not gofmt'd. Print message and fail.
|
||||
|
||||
echo >&2 "Go files must be formatted with $gofmcmd. Please run:"
|
||||
for fn in $unformatted; do
|
||||
echo >&2 " $gofmtcmd -w $PWD/$fn"
|
||||
done
|
||||
|
||||
exit 1
|
||||
@@ -26,6 +26,9 @@ const (
|
||||
|
||||
URLPathUI = "/app"
|
||||
URLPathSubsonicAPI = "/rest"
|
||||
|
||||
RequestThrottleBacklogLimit = 100
|
||||
RequestThrottleBacklogTimeout = time.Minute
|
||||
)
|
||||
|
||||
// Cache options
|
||||
|
||||
@@ -51,6 +51,5 @@ create index annotation_starred
|
||||
}
|
||||
|
||||
func Down20200208222418(tx *sql.Tx) error {
|
||||
// This code is executed when the migration is rolled back.
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,6 +16,5 @@ func Up20200310171621(tx *sql.Tx) error {
|
||||
}
|
||||
|
||||
func Down20200310171621(tx *sql.Tx) error {
|
||||
// This code is executed when the migration is rolled back.
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -37,6 +37,5 @@ drop table if exists search;
|
||||
}
|
||||
|
||||
func Down20200319211049(tx *sql.Tx) error {
|
||||
// This code is executed when the migration is rolled back.
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -75,6 +75,5 @@ create index album_max_year
|
||||
}
|
||||
|
||||
func Down20200327193744(tx *sql.Tx) error {
|
||||
// This code is executed when the migration is rolled back.
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -25,6 +25,5 @@ create index if not exists media_file_track_number
|
||||
}
|
||||
|
||||
func Down20200404214704(tx *sql.Tx) error {
|
||||
// This code is executed when the migration is rolled back.
|
||||
return nil
|
||||
}
|
||||
|
||||
19
db/migration/20200418110522_reindex_to_fix_album_years.go
Normal file
19
db/migration/20200418110522_reindex_to_fix_album_years.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigration(Up20200418110522, Down20200418110522)
|
||||
}
|
||||
|
||||
func Up20200418110522(tx *sql.Tx) error {
|
||||
notice(tx, "A full rescan will be performed to fix search Albums by year")
|
||||
return forceFullRescan(tx)
|
||||
}
|
||||
|
||||
func Down20200418110522(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigration(Up20200419222708, Down20200419222708)
|
||||
}
|
||||
|
||||
func Up20200419222708(tx *sql.Tx) error {
|
||||
notice(tx, "A full rescan will be performed to change the search behaviour")
|
||||
return forceFullRescan(tx)
|
||||
}
|
||||
|
||||
func Down20200419222708(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
64
db/migration/20200423204116_add_sort_fields.go
Normal file
64
db/migration/20200423204116_add_sort_fields.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/pressly/goose"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigration(Up20200423204116, Down20200423204116)
|
||||
}
|
||||
|
||||
func Up20200423204116(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
alter table artist
|
||||
add order_artist_name varchar(255) collate nocase;
|
||||
alter table artist
|
||||
add sort_artist_name varchar(255) collate nocase;
|
||||
create index if not exists artist_order_artist_name
|
||||
on artist (order_artist_name);
|
||||
|
||||
alter table album
|
||||
add order_album_name varchar(255) collate nocase;
|
||||
alter table album
|
||||
add order_album_artist_name varchar(255) collate nocase;
|
||||
alter table album
|
||||
add sort_album_name varchar(255) collate nocase;
|
||||
alter table album
|
||||
add sort_artist_name varchar(255) collate nocase;
|
||||
alter table album
|
||||
add sort_album_artist_name varchar(255) collate nocase;
|
||||
create index if not exists album_order_album_name
|
||||
on album (order_album_name);
|
||||
create index if not exists album_order_album_artist_name
|
||||
on album (order_album_artist_name);
|
||||
|
||||
alter table media_file
|
||||
add order_album_name varchar(255) collate nocase;
|
||||
alter table media_file
|
||||
add order_album_artist_name varchar(255) collate nocase;
|
||||
alter table media_file
|
||||
add order_artist_name varchar(255) collate nocase;
|
||||
alter table media_file
|
||||
add sort_album_name varchar(255) collate nocase;
|
||||
alter table media_file
|
||||
add sort_artist_name varchar(255) collate nocase;
|
||||
alter table media_file
|
||||
add sort_album_artist_name varchar(255) collate nocase;
|
||||
alter table media_file
|
||||
add sort_title varchar(255) collate nocase;
|
||||
create index if not exists media_file_order_album_name
|
||||
on media_file (order_album_name);
|
||||
create index if not exists media_file_order_artist_name
|
||||
on media_file (order_artist_name);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
notice(tx, "A full rescan will be performed to change the search behaviour")
|
||||
return forceFullRescan(tx)
|
||||
}
|
||||
|
||||
func Down20200423204116(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
@@ -9,23 +9,98 @@ import (
|
||||
)
|
||||
|
||||
type ListGenerator interface {
|
||||
GetNewest(ctx context.Context, offset int, size int) (Entries, error)
|
||||
GetRecent(ctx context.Context, offset int, size int) (Entries, error)
|
||||
GetFrequent(ctx context.Context, offset int, size int) (Entries, error)
|
||||
GetHighest(ctx context.Context, offset int, size int) (Entries, error)
|
||||
GetRandom(ctx context.Context, offset int, size int) (Entries, error)
|
||||
GetByName(ctx context.Context, offset int, size int) (Entries, error)
|
||||
GetByArtist(ctx context.Context, offset int, size int) (Entries, error)
|
||||
GetStarred(ctx context.Context, offset int, size int) (Entries, error)
|
||||
GetAllStarred(ctx context.Context) (artists Entries, albums Entries, mediaFiles Entries, err error)
|
||||
GetNowPlaying(ctx context.Context) (Entries, error)
|
||||
GetRandomSongs(ctx context.Context, size int, genre string) (Entries, error)
|
||||
GetSongs(ctx context.Context, offset, size int, filter ListFilter) (Entries, error)
|
||||
GetAlbums(ctx context.Context, offset, size int, filter ListFilter) (Entries, error)
|
||||
}
|
||||
|
||||
func NewListGenerator(ds model.DataStore, npRepo NowPlayingRepository) ListGenerator {
|
||||
return &listGenerator{ds, npRepo}
|
||||
}
|
||||
|
||||
type ListFilter model.QueryOptions
|
||||
|
||||
func ByNewest() ListFilter {
|
||||
return ListFilter{Sort: "createdAt", Order: "desc"}
|
||||
}
|
||||
|
||||
func ByRecent() ListFilter {
|
||||
return ListFilter{Sort: "playDate", Order: "desc", Filters: squirrel.Gt{"play_date": time.Time{}}}
|
||||
}
|
||||
|
||||
func ByFrequent() ListFilter {
|
||||
return ListFilter{Sort: "playCount", Order: "desc", Filters: squirrel.Gt{"play_count": 0}}
|
||||
}
|
||||
|
||||
func ByRandom() ListFilter {
|
||||
return ListFilter{Sort: "random()"}
|
||||
}
|
||||
|
||||
func ByName() ListFilter {
|
||||
return ListFilter{Sort: "name"}
|
||||
}
|
||||
|
||||
func ByArtist() ListFilter {
|
||||
return ListFilter{Sort: "artist"}
|
||||
}
|
||||
|
||||
func ByStarred() ListFilter {
|
||||
return ListFilter{Sort: "starred_at", Order: "desc", Filters: squirrel.Eq{"starred": true}}
|
||||
}
|
||||
|
||||
func ByRating() ListFilter {
|
||||
return ListFilter{Sort: "Rating", Order: "desc", Filters: squirrel.Gt{"rating": 0}}
|
||||
}
|
||||
|
||||
func ByGenre(genre string) ListFilter {
|
||||
return ListFilter{
|
||||
Sort: "genre asc, name asc",
|
||||
Filters: squirrel.Eq{"genre": genre},
|
||||
}
|
||||
}
|
||||
|
||||
func ByYear(fromYear, toYear int) ListFilter {
|
||||
return ListFilter{
|
||||
Sort: "max_year, name",
|
||||
Filters: squirrel.Or{
|
||||
squirrel.And{
|
||||
squirrel.GtOrEq{"min_year": fromYear},
|
||||
squirrel.LtOrEq{"min_year": toYear},
|
||||
},
|
||||
squirrel.And{
|
||||
squirrel.GtOrEq{"max_year": fromYear},
|
||||
squirrel.LtOrEq{"max_year": toYear},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func SongsByGenre(genre string) ListFilter {
|
||||
return ListFilter{
|
||||
Sort: "genre asc, title asc",
|
||||
Filters: squirrel.Eq{"genre": genre},
|
||||
}
|
||||
}
|
||||
|
||||
func SongsByRandom(genre string, fromYear, toYear int) ListFilter {
|
||||
options := ListFilter{
|
||||
Sort: "random()",
|
||||
}
|
||||
ff := squirrel.And{}
|
||||
if genre != "" {
|
||||
ff = append(ff, squirrel.Eq{"genre": genre})
|
||||
}
|
||||
if fromYear != 0 {
|
||||
ff = append(ff, squirrel.GtOrEq{"year": fromYear})
|
||||
}
|
||||
if toYear != 0 {
|
||||
ff = append(ff, squirrel.LtOrEq{"year": toYear})
|
||||
}
|
||||
options.Filters = ff
|
||||
return options
|
||||
}
|
||||
|
||||
type listGenerator struct {
|
||||
ds model.DataStore
|
||||
npRepo NowPlayingRepository
|
||||
@@ -43,54 +118,11 @@ func (g *listGenerator) query(ctx context.Context, qo model.QueryOptions) (Entri
|
||||
return FromAlbums(albums), err
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetNewest(ctx context.Context, offset int, size int) (Entries, error) {
|
||||
qo := model.QueryOptions{Sort: "CreatedAt", Order: "desc", Offset: offset, Max: size}
|
||||
return g.query(ctx, qo)
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetRecent(ctx context.Context, offset int, size int) (Entries, error) {
|
||||
qo := model.QueryOptions{Sort: "PlayDate", Order: "desc", Offset: offset, Max: size,
|
||||
Filters: squirrel.Gt{"play_date": time.Time{}}}
|
||||
return g.query(ctx, qo)
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetFrequent(ctx context.Context, offset int, size int) (Entries, error) {
|
||||
qo := model.QueryOptions{Sort: "PlayCount", Order: "desc", Offset: offset, Max: size,
|
||||
Filters: squirrel.Gt{"play_count": 0}}
|
||||
return g.query(ctx, qo)
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetHighest(ctx context.Context, offset int, size int) (Entries, error) {
|
||||
qo := model.QueryOptions{Sort: "Rating", Order: "desc", Offset: offset, Max: size,
|
||||
Filters: squirrel.Gt{"rating": 0}}
|
||||
return g.query(ctx, qo)
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetByName(ctx context.Context, offset int, size int) (Entries, error) {
|
||||
qo := model.QueryOptions{Sort: "Name", Offset: offset, Max: size}
|
||||
return g.query(ctx, qo)
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetByArtist(ctx context.Context, offset int, size int) (Entries, error) {
|
||||
qo := model.QueryOptions{Sort: "Artist", Offset: offset, Max: size}
|
||||
return g.query(ctx, qo)
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetRandom(ctx context.Context, offset int, size int) (Entries, error) {
|
||||
albums, err := g.ds.Album(ctx).GetRandom(model.QueryOptions{Max: size, Offset: offset})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return FromAlbums(albums), nil
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetRandomSongs(ctx context.Context, size int, genre string) (Entries, error) {
|
||||
options := model.QueryOptions{Max: size}
|
||||
if genre != "" {
|
||||
options.Filters = squirrel.Eq{"genre": genre}
|
||||
}
|
||||
mediaFiles, err := g.ds.MediaFile(ctx).GetRandom(options)
|
||||
func (g *listGenerator) GetSongs(ctx context.Context, offset, size int, filter ListFilter) (Entries, error) {
|
||||
qo := model.QueryOptions(filter)
|
||||
qo.Offset = offset
|
||||
qo.Max = size
|
||||
mediaFiles, err := g.ds.MediaFile(ctx).GetAll(qo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -98,6 +130,18 @@ func (g *listGenerator) GetRandomSongs(ctx context.Context, size int, genre stri
|
||||
return FromMediaFiles(mediaFiles), nil
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetAlbums(ctx context.Context, offset, size int, filter ListFilter) (Entries, error) {
|
||||
qo := model.QueryOptions(filter)
|
||||
qo.Offset = offset
|
||||
qo.Max = size
|
||||
albums, err := g.ds.Album(ctx).GetAll(qo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return FromAlbums(albums), nil
|
||||
}
|
||||
|
||||
func (g *listGenerator) GetStarred(ctx context.Context, offset int, size int) (Entries, error) {
|
||||
qo := model.QueryOptions{Offset: offset, Max: size, Sort: "starred_at", Order: "desc"}
|
||||
albums, err := g.ds.Album(ctx).GetStarred(qo)
|
||||
|
||||
6
go.mod
6
go.mod
@@ -11,11 +11,11 @@ require (
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/dhowden/tag v0.0.0-20200412032933-5d76b8eaae27
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/djherbis/fscache v0.10.0
|
||||
github.com/djherbis/fscache v0.10.1
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/fatih/camelcase v0.0.0-20160318181535-f6a740d52f96 // indirect
|
||||
github.com/fatih/structs v1.0.0 // indirect
|
||||
github.com/go-chi/chi v4.1.0+incompatible
|
||||
github.com/go-chi/chi v4.1.1+incompatible
|
||||
github.com/go-chi/cors v1.1.1
|
||||
github.com/go-chi/jwtauth v4.0.4+incompatible
|
||||
github.com/go-sql-driver/mysql v1.5.0 // indirect
|
||||
@@ -39,6 +39,6 @@ require (
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/djherbis/atime.v1 v1.0.0 // indirect
|
||||
gopkg.in/djherbis/stream.v1 v1.2.0 // indirect
|
||||
gopkg.in/djherbis/stream.v1 v1.3.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
)
|
||||
|
||||
12
go.sum
12
go.sum
@@ -30,8 +30,8 @@ github.com/dhowden/tag v0.0.0-20200412032933-5d76b8eaae27 h1:Z6xaGRBbqfLR797upHu
|
||||
github.com/dhowden/tag v0.0.0-20200412032933-5d76b8eaae27/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/djherbis/fscache v0.10.0 h1:+O3s3LwKL1Jfz7txRHpgKOz8/krh9w+NzfzxtFdbsQg=
|
||||
github.com/djherbis/fscache v0.10.0/go.mod h1:yyPYtkNnnPXsW+81lAcQS6yab3G2CRfnPLotBvtbf0c=
|
||||
github.com/djherbis/fscache v0.10.1 h1:hDv+RGyvD+UDKyRYuLoVNbuRTnf2SrA2K3VyR1br9lk=
|
||||
github.com/djherbis/fscache v0.10.1/go.mod h1:yyPYtkNnnPXsW+81lAcQS6yab3G2CRfnPLotBvtbf0c=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8=
|
||||
@@ -43,8 +43,8 @@ github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU=
|
||||
github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-chi/chi v4.1.0+incompatible h1:ETj3cggsVIY2Xao5ExCu6YhEh5MD6JTfcBzS37R260w=
|
||||
github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/chi v4.1.1+incompatible h1:MmTgB0R8Bt/jccxp+t6S/1VGIKdJw5J74CK/c9tTfA4=
|
||||
github.com/go-chi/chi v4.1.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/cors v1.1.1 h1:eHuqxsIw89iXcWnWUN8R72JMibABJTN/4IOYI5WERvw=
|
||||
github.com/go-chi/cors v1.1.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
|
||||
github.com/go-chi/jwtauth v4.0.4+incompatible h1:LGIxg6YfvSBzxU2BljXbrzVc1fMlgqSKBQgKOGAVtPY=
|
||||
@@ -181,8 +181,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/djherbis/atime.v1 v1.0.0 h1:eMRqB/JrLKocla2PBPKgQYg/p5UG4L6AUAs92aP7F60=
|
||||
gopkg.in/djherbis/atime.v1 v1.0.0/go.mod h1:hQIUStKmJfvf7xdh/wtK84qe+DsTV5LnA9lzxxtPpJ8=
|
||||
gopkg.in/djherbis/stream.v1 v1.2.0 h1:3tZuXO+RK8opjw8/BJr780h+eAPwOFfLHCKRKyYxk3s=
|
||||
gopkg.in/djherbis/stream.v1 v1.2.0/go.mod h1:aEV8CBVRmSpLamVJfM903Npic1IKmb2qS30VAZ+sssg=
|
||||
gopkg.in/djherbis/stream.v1 v1.3.1 h1:uGfmsOY1qqMjQQphhRBSGLyA9qumJ56exkRu9ASTjCw=
|
||||
gopkg.in/djherbis/stream.v1 v1.3.1/go.mod h1:aEV8CBVRmSpLamVJfM903Npic1IKmb2qS30VAZ+sssg=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
|
||||
@@ -3,23 +3,28 @@ package model
|
||||
import "time"
|
||||
|
||||
type Album struct {
|
||||
ID string `json:"id" orm:"column(id)"`
|
||||
Name string `json:"name"`
|
||||
CoverArtPath string `json:"coverArtPath"`
|
||||
CoverArtId string `json:"coverArtId"`
|
||||
ArtistID string `json:"artistId" orm:"pk;column(artist_id)"`
|
||||
Artist string `json:"artist"`
|
||||
AlbumArtistID string `json:"albumArtistId" orm:"pk;column(album_artist_id)"`
|
||||
AlbumArtist string `json:"albumArtist"`
|
||||
MaxYear int `json:"maxYear"`
|
||||
MinYear int `json:"minYear"`
|
||||
Compilation bool `json:"compilation"`
|
||||
SongCount int `json:"songCount"`
|
||||
Duration float32 `json:"duration"`
|
||||
Genre string `json:"genre"`
|
||||
FullText string `json:"fullText"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID string `json:"id" orm:"column(id)"`
|
||||
Name string `json:"name"`
|
||||
CoverArtPath string `json:"coverArtPath"`
|
||||
CoverArtId string `json:"coverArtId"`
|
||||
ArtistID string `json:"artistId" orm:"pk;column(artist_id)"`
|
||||
Artist string `json:"artist"`
|
||||
AlbumArtistID string `json:"albumArtistId" orm:"pk;column(album_artist_id)"`
|
||||
AlbumArtist string `json:"albumArtist"`
|
||||
MaxYear int `json:"maxYear"`
|
||||
MinYear int `json:"minYear"`
|
||||
Compilation bool `json:"compilation"`
|
||||
SongCount int `json:"songCount"`
|
||||
Duration float32 `json:"duration"`
|
||||
Genre string `json:"genre"`
|
||||
FullText string `json:"fullText"`
|
||||
SortAlbumName string `json:"sortAlbumName"`
|
||||
SortArtistName string `json:"sortArtistName"`
|
||||
SortAlbumArtistName string `json:"sortAlbumArtistName"`
|
||||
OrderAlbumName string `json:"orderAlbumName"`
|
||||
OrderAlbumArtistName string `json:"orderAlbumArtistName"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
|
||||
// Annotations
|
||||
PlayCount int `json:"playCount" orm:"-"`
|
||||
|
||||
@@ -3,10 +3,12 @@ package model
|
||||
import "time"
|
||||
|
||||
type Artist struct {
|
||||
ID string `json:"id" orm:"column(id)"`
|
||||
Name string `json:"name"`
|
||||
AlbumCount int `json:"albumCount" orm:"column(album_count)"`
|
||||
FullText string `json:"fullText"`
|
||||
ID string `json:"id" orm:"column(id)"`
|
||||
Name string `json:"name"`
|
||||
AlbumCount int `json:"albumCount" orm:"column(album_count)"`
|
||||
FullText string `json:"fullText"`
|
||||
SortArtistName string `json:"sortArtistName"`
|
||||
OrderArtistName string `json:"orderArtistName"`
|
||||
|
||||
// Annotations
|
||||
PlayCount int `json:"playCount" orm:"-"`
|
||||
|
||||
@@ -6,28 +6,35 @@ import (
|
||||
)
|
||||
|
||||
type MediaFile struct {
|
||||
ID string `json:"id" orm:"pk;column(id)"`
|
||||
Path string `json:"path"`
|
||||
Title string `json:"title"`
|
||||
Album string `json:"album"`
|
||||
ArtistID string `json:"artistId" orm:"pk;column(artist_id)"`
|
||||
Artist string `json:"artist"`
|
||||
AlbumArtistID string `json:"albumArtistId"`
|
||||
AlbumArtist string `json:"albumArtist"`
|
||||
AlbumID string `json:"albumId" orm:"pk;column(album_id)"`
|
||||
HasCoverArt bool `json:"hasCoverArt"`
|
||||
TrackNumber int `json:"trackNumber"`
|
||||
DiscNumber int `json:"discNumber"`
|
||||
Year int `json:"year"`
|
||||
Size int `json:"size"`
|
||||
Suffix string `json:"suffix"`
|
||||
Duration float32 `json:"duration"`
|
||||
BitRate int `json:"bitRate"`
|
||||
Genre string `json:"genre"`
|
||||
FullText string `json:"fullText"`
|
||||
Compilation bool `json:"compilation"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID string `json:"id" orm:"pk;column(id)"`
|
||||
Path string `json:"path"`
|
||||
Title string `json:"title"`
|
||||
Album string `json:"album"`
|
||||
ArtistID string `json:"artistId" orm:"pk;column(artist_id)"`
|
||||
Artist string `json:"artist"`
|
||||
AlbumArtistID string `json:"albumArtistId"`
|
||||
AlbumArtist string `json:"albumArtist"`
|
||||
AlbumID string `json:"albumId" orm:"pk;column(album_id)"`
|
||||
HasCoverArt bool `json:"hasCoverArt"`
|
||||
TrackNumber int `json:"trackNumber"`
|
||||
DiscNumber int `json:"discNumber"`
|
||||
Year int `json:"year"`
|
||||
Size int `json:"size"`
|
||||
Suffix string `json:"suffix"`
|
||||
Duration float32 `json:"duration"`
|
||||
BitRate int `json:"bitRate"`
|
||||
Genre string `json:"genre"`
|
||||
FullText string `json:"fullText"`
|
||||
SortTitle string `json:"sortTitle"`
|
||||
SortAlbumName string `json:"sortAlbumName"`
|
||||
SortArtistName string `json:"sortArtistName"`
|
||||
SortAlbumArtistName string `json:"sortAlbumArtistName"`
|
||||
OrderAlbumName string `json:"orderAlbumName"`
|
||||
OrderArtistName string `json:"orderArtistName"`
|
||||
OrderAlbumArtistName string `json:"orderAlbumArtistName"`
|
||||
Compilation bool `json:"compilation"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
|
||||
// Annotations
|
||||
PlayCount int `json:"playCount" orm:"-"`
|
||||
@@ -48,6 +55,7 @@ type MediaFileRepository interface {
|
||||
Exists(id string) (bool, error)
|
||||
Put(m *MediaFile) error
|
||||
Get(id string) (*MediaFile, error)
|
||||
GetAll(options ...QueryOptions) (MediaFiles, error)
|
||||
FindByAlbum(albumId string) (MediaFiles, error)
|
||||
FindByPath(path string) (MediaFiles, error)
|
||||
GetStarred(options ...QueryOptions) (MediaFiles, error)
|
||||
|
||||
@@ -2,6 +2,9 @@ package persistence
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
. "github.com/Masterminds/squirrel"
|
||||
@@ -23,7 +26,7 @@ func NewAlbumRepository(ctx context.Context, o orm.Ormer) model.AlbumRepository
|
||||
r.ormer = o
|
||||
r.tableName = "album"
|
||||
r.sortMappings = map[string]string{
|
||||
"artist": "compilation asc, album_artist asc, name asc",
|
||||
"artist": "compilation asc, order_album_artist_name asc, order_album_name asc",
|
||||
"random": "RANDOM()",
|
||||
}
|
||||
r.filterMappings = map[string]filterFunc{
|
||||
@@ -108,12 +111,15 @@ func (r *albumRepository) Refresh(ids ...string) error {
|
||||
CurrentId string
|
||||
HasCoverArt bool
|
||||
SongArtists string
|
||||
Years string
|
||||
}
|
||||
var albums []refreshAlbum
|
||||
sel := Select(`album_id as id, album as name, f.artist, f.album_artist, f.artist_id, f.album_artist_id,
|
||||
f.compilation, f.genre, max(f.year) as max_year, min(f.year) as min_year, sum(f.duration) as duration,
|
||||
count(*) as song_count, a.id as current_id, f.id as cover_art_id, f.path as cover_art_path,
|
||||
group_concat(f.artist, ' ') as song_artists, f.has_cover_art`).
|
||||
f.sort_album_name, f.sort_artist_name, f.sort_album_artist_name,
|
||||
f.order_album_name, f.order_album_artist_name,
|
||||
f.compilation, f.genre, max(f.year) as max_year, sum(f.duration) as duration,
|
||||
count(*) as song_count, a.id as current_id, f.id as cover_art_id, f.path as cover_art_path, f.has_cover_art,
|
||||
group_concat(f.artist, ' ') as song_artists, group_concat(f.year, ' ') as years`).
|
||||
From("media_file f").
|
||||
LeftJoin("album a on f.album_id = a.id").
|
||||
Where(Eq{"f.album_id": ids}).GroupBy("album_id").OrderBy("f.id")
|
||||
@@ -136,6 +142,7 @@ func (r *albumRepository) Refresh(ids ...string) error {
|
||||
al.AlbumArtist = al.Artist
|
||||
al.AlbumArtistID = al.ArtistID
|
||||
}
|
||||
al.MinYear = getMinYear(al.Years)
|
||||
al.UpdatedAt = time.Now()
|
||||
if al.CurrentId != "" {
|
||||
toUpdate++
|
||||
@@ -143,7 +150,8 @@ func (r *albumRepository) Refresh(ids ...string) error {
|
||||
toInsert++
|
||||
al.CreatedAt = time.Now()
|
||||
}
|
||||
al.FullText = r.getFullText(al.Name, al.Artist, al.AlbumArtist, al.SongArtists)
|
||||
al.FullText = getFullText(al.Name, al.Artist, al.AlbumArtist, al.SongArtists,
|
||||
al.SortAlbumName, al.SortArtistName, al.SortAlbumArtistName)
|
||||
_, err := r.put(al.ID, al.Album)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -158,6 +166,18 @@ func (r *albumRepository) Refresh(ids ...string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func getMinYear(years string) int {
|
||||
ys := strings.Fields(years)
|
||||
sort.Strings(ys)
|
||||
for _, y := range ys {
|
||||
if y != "0" {
|
||||
r, _ := strconv.Atoi(y)
|
||||
return r
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r *albumRepository) PurgeEmpty() error {
|
||||
del := Delete(r.tableName).Where("id not in (select distinct(album_id) from media_file)")
|
||||
c, err := r.executeSQL(del)
|
||||
|
||||
@@ -20,7 +20,7 @@ var _ = Describe("AlbumRepository", func() {
|
||||
|
||||
Describe("Get", func() {
|
||||
It("returns an existent album", func() {
|
||||
Expect(repo.Get("3")).To(Equal(&albumRadioactivity))
|
||||
Expect(repo.Get("103")).To(Equal(&albumRadioactivity))
|
||||
})
|
||||
It("returns ErrNotFound when the album does not exist", func() {
|
||||
_, err := repo.Get("666")
|
||||
@@ -73,4 +73,16 @@ var _ = Describe("AlbumRepository", func() {
|
||||
})
|
||||
})
|
||||
|
||||
Describe("getMinYear", func() {
|
||||
It("returns 0 when there's no valid year", func() {
|
||||
Expect(getMinYear("a b c")).To(Equal(0))
|
||||
Expect(getMinYear("")).To(Equal(0))
|
||||
})
|
||||
It("returns 0 when all values are 0", func() {
|
||||
Expect(getMinYear("0 0 0 ")).To(Equal(0))
|
||||
})
|
||||
It("returns the smallest value from the list", func() {
|
||||
Expect(getMinYear("2000 0 1800")).To(Equal(1800))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,6 +26,9 @@ func NewArtistRepository(ctx context.Context, o orm.Ormer) model.ArtistRepositor
|
||||
r.ormer = o
|
||||
r.indexGroups = utils.ParseIndexGroups(conf.Server.IndexGroups)
|
||||
r.tableName = "artist"
|
||||
r.sortMappings = map[string]string{
|
||||
"name": "order_artist_name",
|
||||
}
|
||||
r.filterMappings = map[string]filterFunc{
|
||||
"name": fullTextFilter,
|
||||
}
|
||||
@@ -44,19 +47,8 @@ func (r *artistRepository) Exists(id string) (bool, error) {
|
||||
return r.exists(Select().Where(Eq{"id": id}))
|
||||
}
|
||||
|
||||
func (r *artistRepository) getIndexKey(a *model.Artist) string {
|
||||
name := strings.ToLower(utils.NoArticle(a.Name))
|
||||
for k, v := range r.indexGroups {
|
||||
key := strings.ToLower(k)
|
||||
if strings.HasPrefix(name, key) {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "#"
|
||||
}
|
||||
|
||||
func (r *artistRepository) Put(a *model.Artist) error {
|
||||
a.FullText = r.getFullText(a.Name)
|
||||
a.FullText = getFullText(a.Name, a.SortArtistName)
|
||||
_, err := r.put(a.ID, a)
|
||||
return err
|
||||
}
|
||||
@@ -75,11 +67,21 @@ func (r *artistRepository) GetAll(options ...model.QueryOptions) (model.Artists,
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (r *artistRepository) getIndexKey(a *model.Artist) string {
|
||||
name := strings.ToLower(utils.NoArticle(a.Name))
|
||||
for k, v := range r.indexGroups {
|
||||
key := strings.ToLower(k)
|
||||
if strings.HasPrefix(name, key) {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "#"
|
||||
}
|
||||
|
||||
// TODO Cache the index (recalculate when there are changes to the DB)
|
||||
func (r *artistRepository) GetIndex() (model.ArtistIndexes, error) {
|
||||
sq := r.selectArtist().OrderBy("name")
|
||||
sq := r.selectArtist().OrderBy("order_artist_name")
|
||||
var all model.Artists
|
||||
// TODO Paginate
|
||||
err := r.queryAll(sq, &all)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -111,7 +113,9 @@ func (r *artistRepository) Refresh(ids ...string) error {
|
||||
CurrentId string
|
||||
}
|
||||
var artists []refreshArtist
|
||||
sel := Select("f.album_artist_id as id", "f.album_artist as name", "count(*) as album_count", "a.id as current_id").
|
||||
sel := Select("f.album_artist_id as id", "f.album_artist as name", "count(*) as album_count", "a.id as current_id",
|
||||
"f.sort_album_artist_name as sort_artist_name",
|
||||
"f.order_album_artist_name as order_artist_name").
|
||||
From("album f").
|
||||
LeftJoin("artist a on f.album_artist_id = a.id").
|
||||
Where(Eq{"f.album_artist_id": ids}).
|
||||
|
||||
@@ -23,8 +23,8 @@ func NewMediaFileRepository(ctx context.Context, o orm.Ormer) *mediaFileReposito
|
||||
r.ormer = o
|
||||
r.tableName = "media_file"
|
||||
r.sortMappings = map[string]string{
|
||||
"artist": "artist asc, album asc, disc_number asc, track_number asc",
|
||||
"album": "album asc, disc_number asc, track_number asc",
|
||||
"artist": "order_artist_name asc, album asc, disc_number asc, track_number asc",
|
||||
"album": "order_album_name asc, disc_number asc, track_number asc",
|
||||
}
|
||||
r.filterMappings = map[string]filterFunc{
|
||||
"title": fullTextFilter,
|
||||
@@ -41,7 +41,8 @@ func (r mediaFileRepository) Exists(id string) (bool, error) {
|
||||
}
|
||||
|
||||
func (r mediaFileRepository) Put(m *model.MediaFile) error {
|
||||
m.FullText = r.getFullText(m.Title, m.Album, m.Artist, m.AlbumArtist)
|
||||
m.FullText = getFullText(m.Title, m.Album, m.Artist, m.AlbumArtist,
|
||||
m.SortTitle, m.SortAlbumName, m.SortArtistName, m.SortAlbumArtistName)
|
||||
_, err := r.put(m.ID, m)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ var _ = Describe("MediaRepository", func() {
|
||||
})
|
||||
|
||||
It("gets mediafile from the DB", func() {
|
||||
Expect(mr.Get("4")).To(Equal(&songAntenna))
|
||||
Expect(mr.Get("1004")).To(Equal(&songAntenna))
|
||||
})
|
||||
|
||||
It("returns ErrNotFound", func() {
|
||||
@@ -39,7 +39,7 @@ var _ = Describe("MediaRepository", func() {
|
||||
})
|
||||
|
||||
It("find mediafiles by album", func() {
|
||||
Expect(mr.FindByAlbum("3")).To(Equal(model.MediaFiles{
|
||||
Expect(mr.FindByAlbum("103")).To(Equal(model.MediaFiles{
|
||||
songRadioactivity,
|
||||
songAntenna,
|
||||
}))
|
||||
|
||||
@@ -31,8 +31,8 @@ func TestPersistence(t *testing.T) {
|
||||
}
|
||||
|
||||
var (
|
||||
artistKraftwerk = model.Artist{ID: "2", Name: "Kraftwerk", AlbumCount: 1, FullText: "kraftwerk"}
|
||||
artistBeatles = model.Artist{ID: "3", Name: "The Beatles", AlbumCount: 2, FullText: "beatles the"}
|
||||
artistKraftwerk = model.Artist{ID: "2", Name: "Kraftwerk", AlbumCount: 1, FullText: " kraftwerk"}
|
||||
artistBeatles = model.Artist{ID: "3", Name: "The Beatles", AlbumCount: 2, FullText: " beatles the"}
|
||||
testArtists = model.Artists{
|
||||
artistKraftwerk,
|
||||
artistBeatles,
|
||||
@@ -40,9 +40,9 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
albumSgtPeppers = model.Album{ID: "1", Name: "Sgt Peppers", Artist: "The Beatles", AlbumArtistID: "3", Genre: "Rock", CoverArtId: "1", CoverArtPath: P("/beatles/1/sgt/a day.mp3"), SongCount: 1, MaxYear: 1967, FullText: "beatles peppers sgt the"}
|
||||
albumAbbeyRoad = model.Album{ID: "2", Name: "Abbey Road", Artist: "The Beatles", AlbumArtistID: "3", Genre: "Rock", CoverArtId: "2", CoverArtPath: P("/beatles/1/come together.mp3"), SongCount: 1, MaxYear: 1969, FullText: "abbey beatles road the"}
|
||||
albumRadioactivity = model.Album{ID: "3", Name: "Radioactivity", Artist: "Kraftwerk", AlbumArtistID: "2", Genre: "Electronic", CoverArtId: "3", CoverArtPath: P("/kraft/radio/radio.mp3"), SongCount: 2, FullText: "kraftwerk radioactivity"}
|
||||
albumSgtPeppers = model.Album{ID: "101", Name: "Sgt Peppers", Artist: "The Beatles", AlbumArtistID: "3", Genre: "Rock", CoverArtId: "1", CoverArtPath: P("/beatles/1/sgt/a day.mp3"), SongCount: 1, MaxYear: 1967, FullText: " beatles peppers sgt the"}
|
||||
albumAbbeyRoad = model.Album{ID: "102", Name: "Abbey Road", Artist: "The Beatles", AlbumArtistID: "3", Genre: "Rock", CoverArtId: "2", CoverArtPath: P("/beatles/1/come together.mp3"), SongCount: 1, MaxYear: 1969, FullText: " abbey beatles road the"}
|
||||
albumRadioactivity = model.Album{ID: "103", Name: "Radioactivity", Artist: "Kraftwerk", AlbumArtistID: "2", Genre: "Electronic", CoverArtId: "3", CoverArtPath: P("/kraft/radio/radio.mp3"), SongCount: 2, FullText: " kraftwerk radioactivity"}
|
||||
testAlbums = model.Albums{
|
||||
albumSgtPeppers,
|
||||
albumAbbeyRoad,
|
||||
@@ -51,10 +51,10 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
songDayInALife = model.MediaFile{ID: "1", Title: "A Day In A Life", ArtistID: "3", Artist: "The Beatles", AlbumID: "1", Album: "Sgt Peppers", Genre: "Rock", Path: P("/beatles/1/sgt/a day.mp3"), FullText: "a beatles day in life peppers sgt the"}
|
||||
songComeTogether = model.MediaFile{ID: "2", Title: "Come Together", ArtistID: "3", Artist: "The Beatles", AlbumID: "2", Album: "Abbey Road", Genre: "Rock", Path: P("/beatles/1/come together.mp3"), FullText: "abbey beatles come road the together"}
|
||||
songRadioactivity = model.MediaFile{ID: "3", Title: "Radioactivity", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "3", Album: "Radioactivity", Genre: "Electronic", Path: P("/kraft/radio/radio.mp3"), FullText: "kraftwerk radioactivity"}
|
||||
songAntenna = model.MediaFile{ID: "4", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "3", Genre: "Electronic", Path: P("/kraft/radio/antenna.mp3"), FullText: "antenna kraftwerk"}
|
||||
songDayInALife = model.MediaFile{ID: "1001", Title: "A Day In A Life", ArtistID: "3", Artist: "The Beatles", AlbumID: "101", Album: "Sgt Peppers", Genre: "Rock", Path: P("/beatles/1/sgt/a day.mp3"), FullText: " a beatles day in life peppers sgt the"}
|
||||
songComeTogether = model.MediaFile{ID: "1002", Title: "Come Together", ArtistID: "3", Artist: "The Beatles", AlbumID: "102", Album: "Abbey Road", Genre: "Rock", Path: P("/beatles/1/come together.mp3"), FullText: " abbey beatles come road the together"}
|
||||
songRadioactivity = model.MediaFile{ID: "1003", Title: "Radioactivity", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "103", Album: "Radioactivity", Genre: "Electronic", Path: P("/kraft/radio/radio.mp3"), FullText: " kraftwerk radioactivity"}
|
||||
songAntenna = model.MediaFile{ID: "1004", Title: "Antenna", ArtistID: "2", Artist: "Kraftwerk", AlbumID: "103", Genre: "Electronic", Path: P("/kraft/radio/antenna.mp3"), FullText: " antenna kraftwerk"}
|
||||
testSongs = model.MediaFiles{
|
||||
songDayInALife,
|
||||
songComeTogether,
|
||||
@@ -70,9 +70,9 @@ var (
|
||||
Comment: "No Comments",
|
||||
Owner: "userid",
|
||||
Public: true,
|
||||
Tracks: model.MediaFiles{{ID: "1"}, {ID: "3"}},
|
||||
Tracks: model.MediaFiles{{ID: "1001"}, {ID: "1003"}},
|
||||
}
|
||||
plsCool = model.Playlist{ID: "11", Name: "Cool", Tracks: model.MediaFiles{{ID: "4"}}}
|
||||
plsCool = model.Playlist{ID: "11", Name: "Cool", Tracks: model.MediaFiles{{ID: "1004"}}}
|
||||
testPlaylists = model.Playlists{plsBest, plsCool}
|
||||
)
|
||||
|
||||
|
||||
@@ -63,19 +63,19 @@ var _ = Describe("PlaylistRepository", func() {
|
||||
Describe("Put/Exists/Delete", func() {
|
||||
var newPls model.Playlist
|
||||
BeforeEach(func() {
|
||||
newPls = model.Playlist{ID: "22", Name: "Great!", Tracks: model.MediaFiles{{ID: "4"}, {ID: "3"}}}
|
||||
newPls = model.Playlist{ID: "22", Name: "Great!", Tracks: model.MediaFiles{{ID: "1004"}, {ID: "1003"}}}
|
||||
})
|
||||
It("saves the playlist to the DB", func() {
|
||||
Expect(repo.Put(&newPls)).To(BeNil())
|
||||
})
|
||||
It("adds repeated songs to a playlist and keeps the order", func() {
|
||||
newPls.Tracks = append(newPls.Tracks, model.MediaFile{ID: "4"})
|
||||
newPls.Tracks = append(newPls.Tracks, model.MediaFile{ID: "1004"})
|
||||
Expect(repo.Put(&newPls)).To(BeNil())
|
||||
saved, _ := repo.Get("22")
|
||||
Expect(saved.Tracks).To(HaveLen(3))
|
||||
Expect(saved.Tracks[0].ID).To(Equal("4"))
|
||||
Expect(saved.Tracks[1].ID).To(Equal("3"))
|
||||
Expect(saved.Tracks[2].ID).To(Equal("4"))
|
||||
Expect(saved.Tracks[0].ID).To(Equal("1004"))
|
||||
Expect(saved.Tracks[1].ID).To(Equal("1003"))
|
||||
Expect(saved.Tracks[2].ID).To(Equal("1004"))
|
||||
})
|
||||
It("returns the newly created playlist", func() {
|
||||
Expect(repo.Exists("22")).To(BeTrue())
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
. "github.com/Masterminds/squirrel"
|
||||
"github.com/deluan/navidrome/model"
|
||||
"github.com/deluan/rest"
|
||||
"github.com/kennygrant/sanitize"
|
||||
)
|
||||
|
||||
type filterFunc = func(field string, value interface{}) Sqlizer
|
||||
@@ -59,15 +58,11 @@ func booleanFilter(field string, value interface{}) Sqlizer {
|
||||
}
|
||||
|
||||
func fullTextFilter(field string, value interface{}) Sqlizer {
|
||||
q := value.(string)
|
||||
q = strings.TrimSpace(sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*"))))
|
||||
q := sanitizeStrings(value.(string))
|
||||
parts := strings.Split(q, " ")
|
||||
filters := And{}
|
||||
for _, part := range parts {
|
||||
filters = append(filters, Or{
|
||||
Like{"full_text": part + "%"},
|
||||
Like{"full_text": "%" + part + "%"},
|
||||
})
|
||||
filters = append(filters, Like{"full_text": "% " + part + "%"})
|
||||
}
|
||||
return filters
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -8,7 +9,14 @@ import (
|
||||
"github.com/kennygrant/sanitize"
|
||||
)
|
||||
|
||||
func (r sqlRepository) getFullText(text ...string) string {
|
||||
var quotesRegex = regexp.MustCompile("[“”‘’'\"]")
|
||||
|
||||
func getFullText(text ...string) string {
|
||||
fullText := sanitizeStrings(text...)
|
||||
return " " + fullText
|
||||
}
|
||||
|
||||
func sanitizeStrings(text ...string) string {
|
||||
sanitizedText := strings.Builder{}
|
||||
for _, txt := range text {
|
||||
sanitizedText.WriteString(strings.TrimSpace(sanitize.Accents(strings.ToLower(txt))) + " ")
|
||||
@@ -19,14 +27,18 @@ func (r sqlRepository) getFullText(text ...string) string {
|
||||
}
|
||||
var fullText []string
|
||||
for w := range words {
|
||||
fullText = append(fullText, w)
|
||||
w = quotesRegex.ReplaceAllString(w, "")
|
||||
if w != "" {
|
||||
fullText = append(fullText, w)
|
||||
}
|
||||
}
|
||||
sort.Strings(fullText)
|
||||
return strings.Join(fullText, " ")
|
||||
}
|
||||
|
||||
func (r sqlRepository) doSearch(q string, offset, size int, results interface{}, orderBys ...string) error {
|
||||
q = strings.TrimSpace(sanitize.Accents(strings.ToLower(strings.TrimSuffix(q, "*"))))
|
||||
q = strings.TrimSuffix(q, "*")
|
||||
q = sanitizeStrings(q)
|
||||
if len(q) < 2 {
|
||||
return nil
|
||||
}
|
||||
@@ -37,10 +49,7 @@ func (r sqlRepository) doSearch(q string, offset, size int, results interface{},
|
||||
}
|
||||
parts := strings.Split(q, " ")
|
||||
for _, part := range parts {
|
||||
sq = sq.Where(Or{
|
||||
Like{"full_text": part + "%"},
|
||||
Like{"full_text": "%" + part + "%"},
|
||||
})
|
||||
sq = sq.Where(Like{"full_text": "% " + part + "%"})
|
||||
}
|
||||
err := r.queryAll(sq, results)
|
||||
return err
|
||||
|
||||
@@ -6,23 +6,25 @@ import (
|
||||
)
|
||||
|
||||
var _ = Describe("sqlRepository", func() {
|
||||
var sqlRepository = &sqlRepository{}
|
||||
|
||||
Describe("getFullText", func() {
|
||||
It("returns all lowercase chars", func() {
|
||||
Expect(sqlRepository.getFullText("Some Text")).To(Equal("some text"))
|
||||
Expect(getFullText("Some Text")).To(Equal(" some text"))
|
||||
})
|
||||
|
||||
It("removes accents", func() {
|
||||
Expect(sqlRepository.getFullText("Quintão")).To(Equal("quintao"))
|
||||
Expect(getFullText("Quintão")).To(Equal(" quintao"))
|
||||
})
|
||||
|
||||
It("remove extra spaces", func() {
|
||||
Expect(sqlRepository.getFullText(" some text ")).To(Equal("some text"))
|
||||
Expect(getFullText(" some text ")).To(Equal(" some text"))
|
||||
})
|
||||
|
||||
It("remove duplicated words", func() {
|
||||
Expect(sqlRepository.getFullText("legião urbana urbana legiÃo")).To(Equal("legiao urbana"))
|
||||
Expect(getFullText("legião urbana urbana legiÃo")).To(Equal(" legiao urbana"))
|
||||
})
|
||||
|
||||
It("remove symbols", func() {
|
||||
Expect(getFullText("Tom’s Diner ' “40” ‘A’")).To(Equal(" 40 a diner toms"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -61,7 +61,12 @@ func (s *ChangeDetector) loadDir(dirPath string) (children []string, lastUpdated
|
||||
return
|
||||
}
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
isDir, err := IsDirOrSymlinkToDir(dirPath, f)
|
||||
// Skip invalid symlinks
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if isDir {
|
||||
children = append(children, filepath.Join(dirPath, f.Name()))
|
||||
} else {
|
||||
if f.ModTime().After(lastUpdated) {
|
||||
@@ -72,6 +77,26 @@ func (s *ChangeDetector) loadDir(dirPath string) (children []string, lastUpdated
|
||||
return
|
||||
}
|
||||
|
||||
// IsDirOrSymlinkToDir returns true if and only if the Dirent represents a file
|
||||
// system directory, or a symbolic link to a directory. Note that if the Dirent
|
||||
// is not a directory but is a symbolic link, this method will resolve by
|
||||
// sending a request to the operating system to follow the symbolic link.
|
||||
// Copied from github.com/karrick/godirwalk
|
||||
func IsDirOrSymlinkToDir(baseDir string, info os.FileInfo) (bool, error) {
|
||||
if info.IsDir() {
|
||||
return true, nil
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink == 0 {
|
||||
return false, nil
|
||||
}
|
||||
// Does this symlink point to a directory?
|
||||
info, err := os.Stat(filepath.Join(baseDir, info.Name()))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return info.IsDir(), nil
|
||||
}
|
||||
|
||||
func (s *ChangeDetector) loadMap(dirMap dirInfoMap, path string, since time.Time, maybe bool) error {
|
||||
children, lastUpdated, err := s.loadDir(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -110,6 +110,25 @@ var _ = Describe("ChangeDetector", func() {
|
||||
Expect(deleted).To(BeEmpty())
|
||||
Expect(changed).To(ConsistOf(P("a/b")))
|
||||
})
|
||||
|
||||
Describe("IsDirOrSymlinkToDir", func() {
|
||||
It("returns true for normal dirs", func() {
|
||||
dir, _ := os.Stat("tests/fixtures")
|
||||
Expect(IsDirOrSymlinkToDir("tests", dir)).To(BeTrue())
|
||||
})
|
||||
It("returns true for symlinks to dirs", func() {
|
||||
dir, _ := os.Stat("tests/fixtures/symlink2dir")
|
||||
Expect(IsDirOrSymlinkToDir("tests/fixtures", dir)).To(BeTrue())
|
||||
})
|
||||
It("returns false for files", func() {
|
||||
dir, _ := os.Stat("tests/fixtures/test.mp3")
|
||||
Expect(IsDirOrSymlinkToDir("tests/fixtures", dir)).To(BeFalse())
|
||||
})
|
||||
It("returns false for symlinks to files", func() {
|
||||
dir, _ := os.Stat("tests/fixtures/symlink")
|
||||
Expect(IsDirOrSymlinkToDir("tests/fixtures", dir)).To(BeFalse())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// I hate time-based tests....
|
||||
|
||||
@@ -3,6 +3,7 @@ package scanner
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -24,10 +25,16 @@ type Metadata struct {
|
||||
tags map[string]string
|
||||
}
|
||||
|
||||
func (m *Metadata) Title() string { return m.getTag("title", "sort_name") }
|
||||
func (m *Metadata) Album() string { return m.getTag("album", "sort_album") }
|
||||
func (m *Metadata) Artist() string { return m.getTag("artist", "sort_artist") }
|
||||
func (m *Metadata) AlbumArtist() string { return m.getTag("album_artist") }
|
||||
func (m *Metadata) Title() string { return m.getTag("title", "sort_name") }
|
||||
func (m *Metadata) Album() string { return m.getTag("album", "sort_album") }
|
||||
func (m *Metadata) Artist() string { return m.getTag("artist", "sort_artist") }
|
||||
func (m *Metadata) AlbumArtist() string { return m.getTag("album_artist", "albumartist") }
|
||||
func (m *Metadata) SortTitle() string { return m.getSortTag("", "title", "name") }
|
||||
func (m *Metadata) SortAlbum() string { return m.getSortTag("", "album") }
|
||||
func (m *Metadata) SortArtist() string { return m.getSortTag("", "artist") }
|
||||
func (m *Metadata) SortAlbumArtist() string {
|
||||
return m.getSortTag("tso2", "albumartist", "album_artist")
|
||||
}
|
||||
func (m *Metadata) Composer() string { return m.getTag("composer", "tcm", "sort_composer") }
|
||||
func (m *Metadata) Genre() string { return m.getTag("genre") }
|
||||
func (m *Metadata) Year() int { return m.parseYear("date") }
|
||||
@@ -99,7 +106,7 @@ var (
|
||||
inputRegex = regexp.MustCompile(`(?m)^Input #\d+,.*,\sfrom\s'(.*)'`)
|
||||
|
||||
// TITLE : Back In Black
|
||||
tagsRx = regexp.MustCompile(`(?i)^\s{4,6}(\w+)\s*:(.*)`)
|
||||
tagsRx = regexp.MustCompile(`(?i)^\s{4,6}([\w-]+)\s*:(.*)`)
|
||||
|
||||
// Duration: 00:04:16.00, start: 0.000000, bitrate: 995 kb/s`
|
||||
durationRx = regexp.MustCompile(`^\s\sDuration: ([\d.:]+).*bitrate: (\d+)`)
|
||||
@@ -212,7 +219,7 @@ func (m *Metadata) parseYear(tagName string) int {
|
||||
if v, ok := m.tags[tagName]; ok {
|
||||
match := dateRegex.FindStringSubmatch(v)
|
||||
if len(match) == 0 {
|
||||
log.Error("Error parsing year from ffmpeg date field. Please report this issue", "file", m.filePath, "date", v)
|
||||
log.Warn("Error parsing year from ffmpeg date field", "file", m.filePath, "date", v)
|
||||
return 0
|
||||
}
|
||||
year, _ := strconv.Atoi(match[1])
|
||||
@@ -230,6 +237,18 @@ func (m *Metadata) getTag(tags ...string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Metadata) getSortTag(originalTag string, tags ...string) string {
|
||||
formats := []string{"sort%s", "sort_%s", "sort-%s", "%ssort", "%s_sort", "%s-sort"}
|
||||
all := []string{originalTag}
|
||||
for _, tag := range tags {
|
||||
for _, format := range formats {
|
||||
name := fmt.Sprintf(format, tag)
|
||||
all = append(all, name)
|
||||
}
|
||||
}
|
||||
return m.getTag(all...)
|
||||
}
|
||||
|
||||
func (m *Metadata) parseTuple(tags ...string) (int, int) {
|
||||
for _, tagName := range tags {
|
||||
if v, ok := m.tags[tagName]; ok {
|
||||
|
||||
@@ -62,7 +62,7 @@ var _ = Describe("Metadata", func() {
|
||||
})
|
||||
|
||||
It("returns empty map if there are no audio files in path", func() {
|
||||
Expect(LoadAllAudioFiles(".")).To(BeEmpty())
|
||||
Expect(LoadAllAudioFiles("tests/empty_folder")).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -204,6 +204,30 @@ Tracklist:
|
||||
md, _ := extractMetadata("tests/fixtures/test.mp3", outputWithMultilineComment)
|
||||
Expect(md.Comment()).To(Equal(expectedComment))
|
||||
})
|
||||
|
||||
It("parses sort tags correctly", func() {
|
||||
const output = `
|
||||
Input #0, mp3, from '/Users/deluan/Downloads/椎名林檎 - 加爾基 精液 栗ノ花 - 2003/02 - ドツペルゲンガー.mp3':
|
||||
Metadata:
|
||||
title-sort : Dopperugengā
|
||||
album : 加爾基 精液 栗ノ花
|
||||
artist : 椎名林檎
|
||||
album_artist : 椎名林檎
|
||||
title : ドツペルゲンガー
|
||||
albumsort : Kalk Samen Kuri No Hana
|
||||
artist_sort : Shiina, Ringo
|
||||
ALBUMARTISTSORT : Shiina, Ringo
|
||||
`
|
||||
md, _ := extractMetadata("tests/fixtures/test.mp3", output)
|
||||
Expect(md.Title()).To(Equal("ドツペルゲンガー"))
|
||||
Expect(md.Album()).To(Equal("加爾基 精液 栗ノ花"))
|
||||
Expect(md.Artist()).To(Equal("椎名林檎"))
|
||||
Expect(md.AlbumArtist()).To(Equal("椎名林檎"))
|
||||
Expect(md.SortTitle()).To(Equal("Dopperugengā"))
|
||||
Expect(md.SortAlbum()).To(Equal("Kalk Samen Kuri No Hana"))
|
||||
Expect(md.SortArtist()).To(Equal("Shiina, Ringo"))
|
||||
Expect(md.SortAlbumArtist()).To(Equal("Shiina, Ringo"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("parseYear", func() {
|
||||
@@ -231,7 +255,7 @@ Tracklist:
|
||||
|
||||
It("creates a valid command line", func() {
|
||||
args := createProbeCommand([]string{"/music library/one.mp3", "/music library/two.mp3"})
|
||||
Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/one.mp3", "-i", "/music library/two.mp3", "-f", "ffmetadata" }))
|
||||
Expect(args).To(Equal([]string{"ffmpeg", "-i", "/music library/one.mp3", "-i", "/music library/two.mp3", "-f", "ffmetadata"}))
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"github.com/deluan/navidrome/consts"
|
||||
"github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/model"
|
||||
"github.com/deluan/navidrome/utils"
|
||||
"github.com/kennygrant/sanitize"
|
||||
)
|
||||
|
||||
type TagScanner struct {
|
||||
@@ -241,7 +243,7 @@ func (s *TagScanner) loadTracks(filePaths []string) (model.MediaFiles, error) {
|
||||
}
|
||||
|
||||
func (s *TagScanner) toMediaFile(md *Metadata) model.MediaFile {
|
||||
mf := model.MediaFile{}
|
||||
mf := &model.MediaFile{}
|
||||
mf.ID = s.trackID(md)
|
||||
mf.Title = s.mapTrackTitle(md)
|
||||
mf.Album = md.Album()
|
||||
@@ -262,12 +264,25 @@ func (s *TagScanner) toMediaFile(md *Metadata) model.MediaFile {
|
||||
mf.Suffix = md.Suffix()
|
||||
mf.Size = md.Size()
|
||||
mf.HasCoverArt = md.HasPicture()
|
||||
mf.SortTitle = md.SortTitle()
|
||||
mf.SortAlbumName = md.SortAlbum()
|
||||
mf.SortArtistName = md.SortArtist()
|
||||
mf.SortAlbumArtistName = md.SortAlbumArtist()
|
||||
mf.OrderAlbumName = sanitizeFieldForSorting(mf.Album)
|
||||
mf.OrderArtistName = sanitizeFieldForSorting(mf.Artist)
|
||||
mf.OrderAlbumArtistName = sanitizeFieldForSorting(mf.AlbumArtist)
|
||||
|
||||
// TODO Get Creation time. https://github.com/djherbis/times ?
|
||||
mf.CreatedAt = md.ModificationTime()
|
||||
mf.UpdatedAt = md.ModificationTime()
|
||||
|
||||
return mf
|
||||
return *mf
|
||||
}
|
||||
|
||||
func sanitizeFieldForSorting(originalValue string) string {
|
||||
v := utils.NoArticle(originalValue)
|
||||
v = strings.TrimSpace(sanitize.Accents(v))
|
||||
return utils.NoArticle(v)
|
||||
}
|
||||
|
||||
func (s *TagScanner) mapTrackTitle(md *Metadata) string {
|
||||
|
||||
21
scanner/tag_scanner_test.go
Normal file
21
scanner/tag_scanner_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"github.com/deluan/navidrome/conf"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("TagScanner", func() {
|
||||
Describe("sanitizeFieldForSorting", func() {
|
||||
BeforeEach(func() {
|
||||
conf.Server.IgnoredArticles = "The"
|
||||
})
|
||||
It("sanitize accents", func() {
|
||||
Expect(sanitizeFieldForSorting("Céu")).To(Equal("Ceu"))
|
||||
})
|
||||
It("removes articles", func() {
|
||||
Expect(sanitizeFieldForSorting("The Beatles")).To(Equal("Beatles"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,6 @@
|
||||
package subsonic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
@@ -12,37 +11,45 @@ import (
|
||||
)
|
||||
|
||||
type AlbumListController struct {
|
||||
listGen engine.ListGenerator
|
||||
listFunctions map[string]strategy
|
||||
listGen engine.ListGenerator
|
||||
}
|
||||
|
||||
func NewAlbumListController(listGen engine.ListGenerator) *AlbumListController {
|
||||
c := &AlbumListController{
|
||||
listGen: listGen,
|
||||
}
|
||||
c.listFunctions = map[string]strategy{
|
||||
"random": c.listGen.GetRandom,
|
||||
"newest": c.listGen.GetNewest,
|
||||
"recent": c.listGen.GetRecent,
|
||||
"frequent": c.listGen.GetFrequent,
|
||||
"highest": c.listGen.GetHighest,
|
||||
"alphabeticalByName": c.listGen.GetByName,
|
||||
"alphabeticalByArtist": c.listGen.GetByArtist,
|
||||
"starred": c.listGen.GetStarred,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type strategy func(ctx context.Context, offset int, size int) (engine.Entries, error)
|
||||
|
||||
func (c *AlbumListController) getAlbumList(r *http.Request) (engine.Entries, error) {
|
||||
func (c *AlbumListController) getNewAlbumList(r *http.Request) (engine.Entries, error) {
|
||||
typ, err := RequiredParamString(r, "type", "Required string parameter 'type' is not present")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
listFunc, found := c.listFunctions[typ]
|
||||
|
||||
if !found {
|
||||
var filter engine.ListFilter
|
||||
switch typ {
|
||||
case "newest":
|
||||
filter = engine.ByNewest()
|
||||
case "recent":
|
||||
filter = engine.ByRecent()
|
||||
case "random":
|
||||
filter = engine.ByRandom()
|
||||
case "alphabeticalByName":
|
||||
filter = engine.ByName()
|
||||
case "alphabeticalByArtist":
|
||||
filter = engine.ByArtist()
|
||||
case "frequent":
|
||||
filter = engine.ByFrequent()
|
||||
case "starred":
|
||||
filter = engine.ByStarred()
|
||||
case "highest":
|
||||
filter = engine.ByRating()
|
||||
case "byGenre":
|
||||
filter = engine.ByGenre(utils.ParamString(r, "genre"))
|
||||
case "byYear":
|
||||
filter = engine.ByYear(utils.ParamInt(r, "fromYear", 0), utils.ParamInt(r, "toYear", 0))
|
||||
default:
|
||||
log.Error(r, "albumList type not implemented", "type", typ)
|
||||
return nil, errors.New("Not implemented!")
|
||||
}
|
||||
@@ -50,7 +57,7 @@ func (c *AlbumListController) getAlbumList(r *http.Request) (engine.Entries, err
|
||||
offset := utils.ParamInt(r, "offset", 0)
|
||||
size := utils.MinInt(utils.ParamInt(r, "size", 10), 500)
|
||||
|
||||
albums, err := listFunc(r.Context(), offset, size)
|
||||
albums, err := c.listGen.GetAlbums(r.Context(), offset, size, filter)
|
||||
if err != nil {
|
||||
log.Error(r, "Error retrieving albums", "error", err)
|
||||
return nil, errors.New("Internal Error")
|
||||
@@ -60,7 +67,7 @@ func (c *AlbumListController) getAlbumList(r *http.Request) (engine.Entries, err
|
||||
}
|
||||
|
||||
func (c *AlbumListController) GetAlbumList(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
||||
albums, err := c.getAlbumList(r)
|
||||
albums, err := c.getNewAlbumList(r)
|
||||
if err != nil {
|
||||
return nil, NewError(responses.ErrorGeneric, err.Error())
|
||||
}
|
||||
@@ -71,7 +78,7 @@ func (c *AlbumListController) GetAlbumList(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
func (c *AlbumListController) GetAlbumList2(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
||||
albums, err := c.getAlbumList(r)
|
||||
albums, err := c.getNewAlbumList(r)
|
||||
if err != nil {
|
||||
return nil, NewError(responses.ErrorGeneric, err.Error())
|
||||
}
|
||||
@@ -134,8 +141,27 @@ func (c *AlbumListController) GetNowPlaying(w http.ResponseWriter, r *http.Reque
|
||||
func (c *AlbumListController) GetRandomSongs(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
||||
size := utils.MinInt(utils.ParamInt(r, "size", 10), 500)
|
||||
genre := utils.ParamString(r, "genre")
|
||||
fromYear := utils.ParamInt(r, "fromYear", 0)
|
||||
toYear := utils.ParamInt(r, "toYear", 0)
|
||||
|
||||
songs, err := c.listGen.GetRandomSongs(r.Context(), size, genre)
|
||||
songs, err := c.listGen.GetSongs(r.Context(), 0, size, engine.SongsByRandom(genre, fromYear, toYear))
|
||||
if err != nil {
|
||||
log.Error(r, "Error retrieving random songs", "error", err)
|
||||
return nil, NewError(responses.ErrorGeneric, "Internal Error")
|
||||
}
|
||||
|
||||
response := NewResponse()
|
||||
response.RandomSongs = &responses.Songs{}
|
||||
response.RandomSongs.Songs = ToChildren(r.Context(), songs)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *AlbumListController) GetSongsByGenre(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
||||
count := utils.MinInt(utils.ParamInt(r, "count", 10), 500)
|
||||
offset := utils.MinInt(utils.ParamInt(r, "offset", 0), 500)
|
||||
genre := utils.ParamString(r, "genre")
|
||||
|
||||
songs, err := c.listGen.GetSongs(r.Context(), offset, count, engine.SongsByGenre(genre))
|
||||
if err != nil {
|
||||
log.Error(r, "Error retrieving random songs", "error", err)
|
||||
return nil, NewError(responses.ErrorGeneric, "Internal Error")
|
||||
|
||||
@@ -18,7 +18,7 @@ type fakeListGen struct {
|
||||
recvSize int
|
||||
}
|
||||
|
||||
func (lg *fakeListGen) GetNewest(ctx context.Context, offset int, size int) (engine.Entries, error) {
|
||||
func (lg *fakeListGen) GetAlbums(ctx context.Context, offset int, size int, filter engine.ListFilter) (engine.Entries, error) {
|
||||
if lg.err != nil {
|
||||
return nil, lg.err
|
||||
}
|
||||
|
||||
@@ -5,15 +5,18 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"github.com/deluan/navidrome/consts"
|
||||
"github.com/deluan/navidrome/engine"
|
||||
"github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/server/subsonic/responses"
|
||||
"github.com/deluan/navidrome/utils"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
)
|
||||
|
||||
const Version = "1.8.0"
|
||||
const Version = "1.10.2"
|
||||
|
||||
type Handler = func(http.ResponseWriter, *http.Request) (*responses.Subsonic, error)
|
||||
|
||||
@@ -86,6 +89,7 @@ func (api *Router) routes() http.Handler {
|
||||
H(withPlayer, "getStarred2", c.GetStarred2)
|
||||
H(withPlayer, "getNowPlaying", c.GetNowPlaying)
|
||||
H(withPlayer, "getRandomSongs", c.GetRandomSongs)
|
||||
H(withPlayer, "getSongsByGenre", c.GetSongsByGenre)
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
c := initMediaAnnotationController(api)
|
||||
@@ -115,8 +119,11 @@ func (api *Router) routes() http.Handler {
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
c := initMediaRetrievalController(api)
|
||||
H(r, "getAvatar", c.GetAvatar)
|
||||
H(r, "getCoverArt", c.GetCoverArt)
|
||||
// configure request throttling
|
||||
maxRequests := utils.MaxInt(2, runtime.NumCPU())
|
||||
withThrottle := r.With(middleware.ThrottleBacklog(maxRequests, consts.RequestThrottleBacklogLimit, consts.RequestThrottleBacklogTimeout))
|
||||
H(withThrottle, "getAvatar", c.GetAvatar)
|
||||
H(withThrottle, "getCoverArt", c.GetCoverArt)
|
||||
})
|
||||
r.Group(func(r chi.Router) {
|
||||
c := initStreamController(api)
|
||||
|
||||
@@ -28,6 +28,7 @@ type Subsonic struct {
|
||||
NowPlaying *NowPlaying `xml:"nowPlaying,omitempty" json:"nowPlaying,omitempty"`
|
||||
Song *Child `xml:"song,omitempty" json:"song,omitempty"`
|
||||
RandomSongs *Songs `xml:"randomSongs,omitempty" json:"randomSongs,omitempty"`
|
||||
SongsByGenre *Songs `xml:"songsByGenre,omitempty" json:"songsByGenre,omitempty"`
|
||||
Genres *Genres `xml:"genres,omitempty" json:"genres,omitempty"`
|
||||
|
||||
// ID3
|
||||
|
||||
0
tests/empty_folder/not_an_audio_file.txt
Normal file
0
tests/empty_folder/not_an_audio_file.txt
Normal file
1
tests/fixtures/symlink
vendored
Symbolic link
1
tests/fixtures/symlink
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
index.html
|
||||
1
tests/fixtures/symlink2dir
vendored
Symbolic link
1
tests/fixtures/symlink2dir
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../
|
||||
1
tests/fixtures/synlink_invalid
vendored
Symbolic link
1
tests/fixtures/synlink_invalid
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
INVALID
|
||||
591
ui/package-lock.json
generated
591
ui/package-lock.json
generated
@@ -1640,9 +1640,10 @@
|
||||
}
|
||||
},
|
||||
"@jest/types": {
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-25.1.0.tgz",
|
||||
"integrity": "sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA==",
|
||||
"version": "25.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-25.4.0.tgz",
|
||||
"integrity": "sha512-XBeaWNzw2PPnGW5aXvZt3+VO60M+34RY3XDsCK5tW7kyj3RK0XClRutCfjqcBuaR2aBQTbluEDME9b5MB9UAPw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^1.1.1",
|
||||
@@ -1651,19 +1652,20 @@
|
||||
}
|
||||
},
|
||||
"@material-ui/core": {
|
||||
"version": "4.9.8",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.9.8.tgz",
|
||||
"integrity": "sha512-4cslpG6oLoPWUfwPkX+hvbak4hAGiOfgXOu/UIYeeMrtsTEebC0Mirjoby7zhS4ny86YI3rXEFW6EZDmlj5n5w==",
|
||||
"version": "4.9.11",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.9.11.tgz",
|
||||
"integrity": "sha512-S2Ha9GpTxzl29XMeMc8dQX2pj97yApNzuhe/23If53fMdg5Fmd3SgbE1bMbyXeKhxwtXZjOFxd0vU+W/sez8Ew==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"@material-ui/styles": "^4.9.6",
|
||||
"@material-ui/system": "^4.9.6",
|
||||
"@material-ui/types": "^5.0.0",
|
||||
"@material-ui/react-transition-group": "^4.2.0",
|
||||
"@material-ui/styles": "^4.9.10",
|
||||
"@material-ui/system": "^4.9.10",
|
||||
"@material-ui/types": "^5.0.1",
|
||||
"@material-ui/utils": "^4.9.6",
|
||||
"@types/react-transition-group": "^4.2.0",
|
||||
"clsx": "^1.0.2",
|
||||
"clsx": "^1.0.4",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"popper.js": "^1.14.1",
|
||||
"popper.js": "^1.16.1-lts",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^16.8.0",
|
||||
"react-transition-group": "^4.3.0"
|
||||
@@ -1687,14 +1689,25 @@
|
||||
"@babel/runtime": "^7.4.4"
|
||||
}
|
||||
},
|
||||
"@material-ui/react-transition-group": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/react-transition-group/-/react-transition-group-4.2.0.tgz",
|
||||
"integrity": "sha512-4zapZ0gW1ZTws5aH9OGy3IMvtTV/olc7YrVSkM1WFu1FsrEhL+qarEniRjx7LjHt0gukFqoINfElI8v2boVMQA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.5",
|
||||
"dom-helpers": "^3.4.0",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"@material-ui/styles": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.9.6.tgz",
|
||||
"integrity": "sha512-ijgwStEkw1OZ6gCz18hkjycpr/3lKs1hYPi88O/AUn4vMuuGEGAIrqKVFq/lADmZUNF3DOFIk8LDkp7zmjPxtA==",
|
||||
"version": "4.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.9.10.tgz",
|
||||
"integrity": "sha512-EXIXlqVyFDnjXF6tj72y6ZxiSy+mHtrsCo3Srkm3XUeu3Z01aftDBy7ZSr3TQ02gXHTvDSBvegp3Le6p/tl7eA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"@emotion/hash": "^0.8.0",
|
||||
"@material-ui/types": "^5.0.0",
|
||||
"@material-ui/types": "^5.0.1",
|
||||
"@material-ui/utils": "^4.9.6",
|
||||
"clsx": "^1.0.2",
|
||||
"csstype": "^2.5.2",
|
||||
@@ -1721,9 +1734,9 @@
|
||||
}
|
||||
},
|
||||
"@material-ui/system": {
|
||||
"version": "4.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.9.6.tgz",
|
||||
"integrity": "sha512-QtfoAePyqXoZ2HUVSwGb1Ro0kucMCvVjbI0CdYIR21t0Opgfm1Oer6ni9P5lfeXA39xSt0wCierw37j+YES48Q==",
|
||||
"version": "4.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.9.10.tgz",
|
||||
"integrity": "sha512-E+t0baX2TBZk6ALm8twG6objpsxLdMM4MDm1++LMt2m7CetCAEc3aIAfDaprk4+tm5hFT1Cah5dRWk8EeIFQYw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.4.4",
|
||||
"@material-ui/utils": "^4.9.6",
|
||||
@@ -1731,9 +1744,9 @@
|
||||
}
|
||||
},
|
||||
"@material-ui/types": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.0.0.tgz",
|
||||
"integrity": "sha512-UeH2BuKkwDndtMSS0qgx1kCzSMw+ydtj0xx/XbFtxNSTlXydKwzs5gVW5ZKsFlAkwoOOQ9TIsyoCC8hq18tOwg=="
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.0.1.tgz",
|
||||
"integrity": "sha512-wURPSY7/3+MAtng3i26g+WKwwNE3HEeqa/trDBR5+zWKmcjO+u9t7Npu/J1r+3dmIa/OeziN9D/18IrBKvKffw=="
|
||||
},
|
||||
"@material-ui/utils": {
|
||||
"version": "4.9.6",
|
||||
@@ -1921,32 +1934,50 @@
|
||||
}
|
||||
},
|
||||
"@testing-library/dom": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.0.4.tgz",
|
||||
"integrity": "sha512-+vrLcGDvopLPsBB7JgJhf8ZoOhBSeCsI44PKJL9YoKrP2AvCkqrTg+z77wEEZJ4tSNdxV0kymil7hSvsQQ7jMQ==",
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.2.1.tgz",
|
||||
"integrity": "sha512-xIGoHlQ2ZiEL1dJIFKNmLDypzYF+4OJTTASRctl/aoIDaS5y/pRVHRigoqvPUV11mdJoR71IIgi/6UviMgyz4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.8.4",
|
||||
"@types/testing-library__dom": "^6.12.1",
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@types/testing-library__dom": "^7.0.0",
|
||||
"aria-query": "^4.0.2",
|
||||
"dom-accessibility-api": "^0.3.0",
|
||||
"dom-accessibility-api": "^0.4.2",
|
||||
"pretty-format": "^25.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz",
|
||||
"integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"aria-query": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.0.2.tgz",
|
||||
"integrity": "sha512-S1G1V790fTaigUSM/Gd0NngzEfiMy9uTUfMyHhKhVyy4cH5O/eTuR01ydhGL0z4Za1PXFTRGH3qL8VhUQuEO5w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.4",
|
||||
"@babel/runtime-corejs3": "^7.7.4"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.5",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
|
||||
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@testing-library/jest-dom": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.3.0.tgz",
|
||||
"integrity": "sha512-Cdhpc3BHL888X55qBNyra9eM0UG63LCm/FqCWTa1Ou/0MpsUbQTM9vW1NU6/jBQFoSLgkFfDG5XVpm2V0dOm/A==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.5.0.tgz",
|
||||
"integrity": "sha512-7sWHrpxG4Yd8TmryI7Rtbx8Ff4mbs3ASye3oshQIuHvsCR+QHgr7rTR/PfeXvOmwUwR36wSTTAvrLKsPmr6VEQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@types/testing-library__jest-dom": "^5.0.2",
|
||||
@@ -1963,6 +1994,7 @@
|
||||
"version": "7.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz",
|
||||
"integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
@@ -1970,24 +2002,27 @@
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.5",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
|
||||
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
|
||||
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@testing-library/react": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-10.0.1.tgz",
|
||||
"integrity": "sha512-sMHWud2dcymOzq2AhEniICSijEwKeTiBX+K0y36FYNY7wH2t0SIP1o732Bf5dDY0jYoMC2hj2UJSVpZC/rDsWg==",
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-10.0.2.tgz",
|
||||
"integrity": "sha512-YT6Mw0oJz7R6vlEkmo1FlUD+K15FeXApOB5Ffm9zooFVnrwkt00w18dUJFMOh1yRp9wTdVRonbor7o4PIpFCmA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"@testing-library/dom": "^7.0.2",
|
||||
"@types/testing-library__react": "^9.1.3"
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@testing-library/dom": "^7.1.0",
|
||||
"@types/testing-library__react": "^10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.8.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.7.tgz",
|
||||
"integrity": "sha512-+AATMUFppJDw6aiR5NVPHqIQBlV/Pj8wY/EZH+lmvRdUo9xBaz/rF3alAwFJQavvKfeOlPE7oaaDHVbcySbCsg==",
|
||||
"version": "7.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz",
|
||||
"integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
@@ -1995,14 +2030,16 @@
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.5",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
|
||||
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
|
||||
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@testing-library/user-event": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-10.0.1.tgz",
|
||||
"integrity": "sha512-M63ftowo1QpAGMnWyz7df0ygqnu4XyF68Sty7mivMAz2HLcY1uLoN3qcen6WMobdY0MoZUi4+BLsziSDAP62Vg=="
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-10.0.2.tgz",
|
||||
"integrity": "sha512-fVeP4U37BIYdp9nBRKEITFSLPqgCSS7Og6LHvxoQ2JSOTJ1NJI4Dfesv4uNXxvNNcJgBS88V+Tc6h8vbDsa2iA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/babel__core": {
|
||||
"version": "7.1.6",
|
||||
@@ -2089,25 +2126,13 @@
|
||||
}
|
||||
},
|
||||
"@types/jest": {
|
||||
"version": "25.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.1.5.tgz",
|
||||
"integrity": "sha512-FBmb9YZHoEOH56Xo/PIYtfuyTL0IzJLM3Hy0Sqc82nn5eqqXgefKcl/eMgChM8eSGVfoDee8cdlj7K74T8a6Yg==",
|
||||
"version": "25.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.2.1.tgz",
|
||||
"integrity": "sha512-msra1bCaAeEdkSyA0CZ6gW1ukMIvZ5YoJkdXw/qhQdsuuDlFTcEUrUw8CLCPt2rVRUfXlClVvK2gvPs9IokZaA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"jest-diff": "25.1.0",
|
||||
"pretty-format": "25.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"jest-diff": {
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.1.0.tgz",
|
||||
"integrity": "sha512-nepXgajT+h017APJTreSieh4zCqnSHEJ1iT8HDlewu630lSJ4Kjjr9KNzm+kzGwwcpsDE6Snx1GJGzzsefaEHw==",
|
||||
"requires": {
|
||||
"chalk": "^3.0.0",
|
||||
"diff-sequences": "^25.1.0",
|
||||
"jest-get-type": "^25.1.0",
|
||||
"pretty-format": "^25.1.0"
|
||||
}
|
||||
}
|
||||
"jest-diff": "^25.2.1",
|
||||
"pretty-format": "^25.2.1"
|
||||
}
|
||||
},
|
||||
"@types/json-schema": {
|
||||
@@ -2150,9 +2175,10 @@
|
||||
}
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"version": "16.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.5.tgz",
|
||||
"integrity": "sha512-BX6RQ8s9D+2/gDhxrj8OW+YD4R+8hj7FEM/OJHGNR0KipE1h1mSsf39YeyC81qafkq+N3rU3h3RFbLSwE5VqUg==",
|
||||
"version": "16.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.6.tgz",
|
||||
"integrity": "sha512-S6ihtlPMDotrlCJE9ST1fRmYrQNNwfgL61UB4I1W7M6kPulUKx9fXAleW5zpdIjUQ4fTaaog8uERezjsGUj9HQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
@@ -2171,82 +2197,28 @@
|
||||
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw=="
|
||||
},
|
||||
"@types/testing-library__dom": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-6.14.0.tgz",
|
||||
"integrity": "sha512-sMl7OSv0AvMOqn1UJ6j1unPMIHRXen0Ita1ujnMX912rrOcawe4f7wu0Zt9GIQhBhJvH2BaibqFgQ3lP+Pj2hA==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-7.0.1.tgz",
|
||||
"integrity": "sha512-WokGRksRJb3Dla6h02/0/NNHTkjsj4S8aJZiwMj/5/UL8VZ1iCe3H8SHzfpmBeH8Vp4SPRT8iC2o9kYULFhDIw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pretty-format": "^24.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jest/types": {
|
||||
"version": "24.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz",
|
||||
"integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==",
|
||||
"requires": {
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^1.1.1",
|
||||
"@types/yargs": "^13.0.0"
|
||||
}
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "13.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz",
|
||||
"integrity": "sha512-XAvHLwG7UQ+8M4caKIH0ZozIOYay5fQkAgyIXegXT9jPtdIGdhga+sUEdAr1CiG46aB+c64xQEYyEzlwWVTNzA==",
|
||||
"requires": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"requires": {
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "24.9.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz",
|
||||
"integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==",
|
||||
"requires": {
|
||||
"@jest/types": "^24.9.0",
|
||||
"ansi-regex": "^4.0.0",
|
||||
"ansi-styles": "^3.2.0",
|
||||
"react-is": "^16.8.4"
|
||||
}
|
||||
}
|
||||
"pretty-format": "^25.1.0"
|
||||
}
|
||||
},
|
||||
"@types/testing-library__jest-dom": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.0.2.tgz",
|
||||
"integrity": "sha512-dZP+/WHndgCSmdaImITy0KhjGAa9c0hlGGkzefbtrPFpnGEPZECDA0zyvfSp8RKhHECJJSKHFExjOwzo0rHyIA==",
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.0.3.tgz",
|
||||
"integrity": "sha512-NdbKc6yseg6uq4UJFwimPws0iwsGugVbPoOTP2EH+PJMJKiZsoSg5F2H3XYweOyytftCOuIMuXifBUrF9CSvaQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/jest": "*"
|
||||
}
|
||||
},
|
||||
"@types/testing-library__react": {
|
||||
"version": "9.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__react/-/testing-library__react-9.1.3.tgz",
|
||||
"integrity": "sha512-iCdNPKU3IsYwRK9JieSYAiX0+aYDXOGAmrC/3/M7AqqSDKnWWVv07X+Zk1uFSL7cMTUYzv4lQRfohucEocn5/w==",
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__react/-/testing-library__react-10.0.1.tgz",
|
||||
"integrity": "sha512-RbDwmActAckbujLZeVO/daSfdL1pnjVqas25UueOkAY5r7vriavWf0Zqg7ghXMHa8ycD/kLkv8QOj31LmSYwww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react-dom": "*",
|
||||
"@types/testing-library__dom": "*",
|
||||
@@ -2257,6 +2229,7 @@
|
||||
"version": "15.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz",
|
||||
"integrity": "sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
@@ -4548,11 +4521,11 @@
|
||||
}
|
||||
},
|
||||
"css-vendor": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.7.tgz",
|
||||
"integrity": "sha512-VS9Rjt79+p7M0WkPqcAza4Yq1ZHrsHrwf7hPL/bjQB+c1lwmAI+1FXxYTYt818D/50fFVflw0XKleiBN5RITkg==",
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz",
|
||||
"integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.6.2",
|
||||
"@babel/runtime": "^7.8.3",
|
||||
"is-in-browser": "^1.0.2"
|
||||
}
|
||||
},
|
||||
@@ -4564,7 +4537,8 @@
|
||||
"css.escape": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||
"integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s="
|
||||
"integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=",
|
||||
"dev": true
|
||||
},
|
||||
"cssdb": {
|
||||
"version": "4.4.0",
|
||||
@@ -4938,7 +4912,8 @@
|
||||
"diff-sequences": {
|
||||
"version": "25.2.6",
|
||||
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz",
|
||||
"integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg=="
|
||||
"integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==",
|
||||
"dev": true
|
||||
},
|
||||
"diffie-hellman": {
|
||||
"version": "5.0.3",
|
||||
@@ -4990,9 +4965,10 @@
|
||||
}
|
||||
},
|
||||
"dom-accessibility-api": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.3.0.tgz",
|
||||
"integrity": "sha512-PzwHEmsRP3IGY4gv/Ug+rMeaTIyTJvadCb+ujYXYeIylbHJezIyNToe8KfEgHTCEYyC+/bUghYOGg8yMGlZ6vA=="
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.4.3.tgz",
|
||||
"integrity": "sha512-JZ8iPuEHDQzq6q0k7PKMGbrIdsgBB7TRrtVOUm4nSMCExlg5qQG4KXWTH2k90yggjM4tTumRGwTKJSldMzKyLA==",
|
||||
"dev": true
|
||||
},
|
||||
"dom-align": {
|
||||
"version": "1.11.1",
|
||||
@@ -5008,27 +4984,11 @@
|
||||
}
|
||||
},
|
||||
"dom-helpers": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz",
|
||||
"integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
|
||||
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^2.6.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz",
|
||||
"integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.5",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
|
||||
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
|
||||
}
|
||||
"@babel/runtime": "^7.1.2"
|
||||
}
|
||||
},
|
||||
"dom-serializer": {
|
||||
@@ -8118,38 +8078,15 @@
|
||||
}
|
||||
},
|
||||
"jest-diff": {
|
||||
"version": "25.2.6",
|
||||
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.2.6.tgz",
|
||||
"integrity": "sha512-KuadXImtRghTFga+/adnNrv9s61HudRMR7gVSbP35UKZdn4IK2/0N0PpGZIqtmllK9aUyye54I3nu28OYSnqOg==",
|
||||
"version": "25.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.4.0.tgz",
|
||||
"integrity": "sha512-kklLbJVXW0y8UKOWOdYhI6TH5MG6QAxrWiBMgQaPIuhj3dNFGirKCd+/xfplBXICQ7fI+3QcqHm9p9lWu1N6ug==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^3.0.0",
|
||||
"diff-sequences": "^25.2.6",
|
||||
"jest-get-type": "^25.2.6",
|
||||
"pretty-format": "^25.2.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jest/types": {
|
||||
"version": "25.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-25.2.6.tgz",
|
||||
"integrity": "sha512-myJTTV37bxK7+3NgKc4Y/DlQ5q92/NOwZsZ+Uch7OXdElxOg61QYc72fPYNAjlvbnJ2YvbXLamIsa9tj48BmyQ==",
|
||||
"requires": {
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^1.1.1",
|
||||
"@types/yargs": "^15.0.0",
|
||||
"chalk": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "25.2.6",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.2.6.tgz",
|
||||
"integrity": "sha512-DEiWxLBaCHneffrIT4B+TpMvkV9RNvvJrd3lY9ew1CEQobDzEXmYT1mg0hJhljZty7kCc10z13ohOFAE8jrUDg==",
|
||||
"requires": {
|
||||
"@jest/types": "^25.2.6",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^4.0.0",
|
||||
"react-is": "^16.12.0"
|
||||
}
|
||||
}
|
||||
"pretty-format": "^25.4.0"
|
||||
}
|
||||
},
|
||||
"jest-docblock": {
|
||||
@@ -8419,7 +8356,8 @@
|
||||
"jest-get-type": {
|
||||
"version": "25.2.6",
|
||||
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz",
|
||||
"integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig=="
|
||||
"integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==",
|
||||
"dev": true
|
||||
},
|
||||
"jest-haste-map": {
|
||||
"version": "24.9.0",
|
||||
@@ -9151,38 +9089,15 @@
|
||||
}
|
||||
},
|
||||
"jest-matcher-utils": {
|
||||
"version": "25.2.6",
|
||||
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.2.6.tgz",
|
||||
"integrity": "sha512-+6IbC98ZBw3X7hsfUvt+7VIYBdI0FEvhSBjWo9XTHOc1KAAHDsrSHdeyHH/Su0r/pf4OEGuWRRLPnjkhS2S19A==",
|
||||
"version": "25.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.4.0.tgz",
|
||||
"integrity": "sha512-yPMdtj7YDgXhnGbc66bowk8AkQ0YwClbbwk3Kzhn5GVDrciiCr27U4NJRbrqXbTdtxjImONITg2LiRIw650k5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^3.0.0",
|
||||
"jest-diff": "^25.2.6",
|
||||
"jest-diff": "^25.4.0",
|
||||
"jest-get-type": "^25.2.6",
|
||||
"pretty-format": "^25.2.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jest/types": {
|
||||
"version": "25.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-25.2.6.tgz",
|
||||
"integrity": "sha512-myJTTV37bxK7+3NgKc4Y/DlQ5q92/NOwZsZ+Uch7OXdElxOg61QYc72fPYNAjlvbnJ2YvbXLamIsa9tj48BmyQ==",
|
||||
"requires": {
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^1.1.1",
|
||||
"@types/yargs": "^15.0.0",
|
||||
"chalk": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "25.2.6",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.2.6.tgz",
|
||||
"integrity": "sha512-DEiWxLBaCHneffrIT4B+TpMvkV9RNvvJrd3lY9ew1CEQobDzEXmYT1mg0hJhljZty7kCc10z13ohOFAE8jrUDg==",
|
||||
"requires": {
|
||||
"@jest/types": "^25.2.6",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^4.0.0",
|
||||
"react-is": "^16.12.0"
|
||||
}
|
||||
}
|
||||
"pretty-format": "^25.4.0"
|
||||
}
|
||||
},
|
||||
"jest-message-util": {
|
||||
@@ -10800,7 +10715,8 @@
|
||||
"min-indent": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz",
|
||||
"integrity": "sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY="
|
||||
"integrity": "sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=",
|
||||
"dev": true
|
||||
},
|
||||
"mini-create-react-context": {
|
||||
"version": "0.3.2",
|
||||
@@ -12825,9 +12741,9 @@
|
||||
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw="
|
||||
},
|
||||
"prettier": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
|
||||
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.4.tgz",
|
||||
"integrity": "sha512-SVJIQ51spzFDvh4fIbCLvciiDMCrRhlN3mbZvv/+ycjvmF5E73bKdGfU8QDLNmjYJf+lsGnDBC4UUnvTe5OO0w==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-bytes": {
|
||||
@@ -12845,11 +12761,12 @@
|
||||
}
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.1.0.tgz",
|
||||
"integrity": "sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ==",
|
||||
"version": "25.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.4.0.tgz",
|
||||
"integrity": "sha512-PI/2dpGjXK5HyXexLPZU/jw5T9Q6S1YVXxxVxco+LIqzUFHXIbKZKdUVt7GcX7QUCr31+3fzhi4gN4/wUYPVxQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/types": "^25.1.0",
|
||||
"@jest/types": "^25.4.0",
|
||||
"ansi-regex": "^5.0.0",
|
||||
"ansi-styles": "^4.0.0",
|
||||
"react-is": "^16.12.0"
|
||||
@@ -13010,9 +12927,9 @@
|
||||
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA=="
|
||||
},
|
||||
"ra-core": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ra-core/-/ra-core-3.3.3.tgz",
|
||||
"integrity": "sha512-SwbKf/qnYfCSTrbjnRo0w6PM3cHcyA6iKNElSqf0OlV6FeXxVrTjuxE5lAbjRaxBKZBE62h7LtBj48z2TjYr/g==",
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ra-core/-/ra-core-3.4.1.tgz",
|
||||
"integrity": "sha512-XHcYAU36aMhIAmspdQ299SLCclu7qMmPS/IXN1VPVlRJHAIurK5tgRUMStAgDRO05qIkU5Xnb9C9PA63VmlPwA==",
|
||||
"requires": {
|
||||
"@testing-library/react": "^8.0.7",
|
||||
"classnames": "~2.2.5",
|
||||
@@ -13105,32 +13022,153 @@
|
||||
}
|
||||
},
|
||||
"ra-data-json-server": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ra-data-json-server/-/ra-data-json-server-3.3.3.tgz",
|
||||
"integrity": "sha512-iOUbrU5bhOa3iEldyRFgk2HarX0h9qgzts7F/zA2UWYKKhpSBVHVI9X3VvYU+lhIJXll1+OjqpEJft5cXQnLRg==",
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ra-data-json-server/-/ra-data-json-server-3.4.1.tgz",
|
||||
"integrity": "sha512-jRTALjGy4tUMYZL6MvrYNvDa+7O0YHEv7aFoc/r4nMX2G1+H/oH3H2BBWKhJeSMz828rhLPAaYTPj2F2NOYmRQ==",
|
||||
"requires": {
|
||||
"query-string": "^5.1.1",
|
||||
"ra-core": "^3.3.3"
|
||||
"ra-core": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"ra-i18n-polyglot": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ra-i18n-polyglot/-/ra-i18n-polyglot-3.3.3.tgz",
|
||||
"integrity": "sha512-dV00IZ5/gLLhTAbcmKeb4F5BsDE1anQMYRR1y6DeZobW4uMDjIX23HPUPGUGi4Cj6Na3M+j+lXqKsjpuQu6ZVg==",
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ra-i18n-polyglot/-/ra-i18n-polyglot-3.4.1.tgz",
|
||||
"integrity": "sha512-i13N/wGi7aOxKeABdJ54wwYJZPUeLLUQ23C1TAbNynFro1pIHl2j4lxRBhRIlYPwNKdsUjuLzt30fdcVIGH+FQ==",
|
||||
"requires": {
|
||||
"node-polyglot": "^2.2.2",
|
||||
"ra-core": "^3.3.3"
|
||||
"ra-core": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"ra-language-chinese": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ra-language-chinese/-/ra-language-chinese-2.0.5.tgz",
|
||||
"integrity": "sha512-BwaqQWDNhQX/Ufe5Ki2GrJ3k5OGmH8dKrQn/npvRik80+tpN4Ew4vbyS8o4E74B4UfSJ8Sj10YdB0bA6FZnAOA=="
|
||||
},
|
||||
"ra-language-english": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ra-language-english/-/ra-language-english-3.2.0.tgz",
|
||||
"integrity": "sha512-/XmwYWoQoB4MBkkzBCbg/ykCuRGjHQOHLk2ik6n1aM10AWHxiiJNyRw2aoLzH7Vc5rcp4BBJQCuhT+DgfYIJ2Q=="
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ra-language-english/-/ra-language-english-3.4.1.tgz",
|
||||
"integrity": "sha512-bAoJyIGL3LJ/8hIvQ+gsHYlFKwgpOalQb3ZUdJReU+vAt52nQqN/3BxknxSMRFOxH0iMQk9k3B+EakDtiesH3Q==",
|
||||
"requires": {
|
||||
"ra-core": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"ra-language-french": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ra-language-french/-/ra-language-french-3.4.1.tgz",
|
||||
"integrity": "sha512-PZh9+n0FDw2VTNQR/H+Q3FXD49J4FAynTxZ1Zp8z3uCVHtlUySJwPQDp/pTbSmLIfGZ5DWfJltK4IqUkikY+0w==",
|
||||
"requires": {
|
||||
"ra-core": "^3.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jest/types": {
|
||||
"version": "24.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz",
|
||||
"integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==",
|
||||
"requires": {
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^1.1.1",
|
||||
"@types/yargs": "^13.0.0"
|
||||
}
|
||||
},
|
||||
"@testing-library/dom": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-5.6.1.tgz",
|
||||
"integrity": "sha512-Y1T2bjtvQMewffn1CJ28kpgnuvPYKsBcZMagEH0ppfEMZPDc8AkkEnTk4smrGZKw0cblNB3lhM2FMnpfLExlHg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@sheerun/mutationobserver-shim": "^0.3.2",
|
||||
"aria-query": "3.0.0",
|
||||
"pretty-format": "^24.8.0",
|
||||
"wait-for-expect": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"@testing-library/react": {
|
||||
"version": "8.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-8.0.9.tgz",
|
||||
"integrity": "sha512-I7zd+MW5wk8rQA5VopZgBfxGKUd91jgZ6Vzj2gMqFf2iGGtKwvI5SVTrIJcSFaOXK88T2EUsbsIKugDtoqOcZQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@testing-library/dom": "^5.6.1"
|
||||
}
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "13.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz",
|
||||
"integrity": "sha512-XAvHLwG7UQ+8M4caKIH0ZozIOYay5fQkAgyIXegXT9jPtdIGdhga+sUEdAr1CiG46aB+c64xQEYyEzlwWVTNzA==",
|
||||
"requires": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"requires": {
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "24.9.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz",
|
||||
"integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==",
|
||||
"requires": {
|
||||
"@jest/types": "^24.9.0",
|
||||
"ansi-regex": "^4.0.0",
|
||||
"ansi-styles": "^3.2.0",
|
||||
"react-is": "^16.8.4"
|
||||
}
|
||||
},
|
||||
"ra-core": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ra-core/-/ra-core-3.4.1.tgz",
|
||||
"integrity": "sha512-XHcYAU36aMhIAmspdQ299SLCclu7qMmPS/IXN1VPVlRJHAIurK5tgRUMStAgDRO05qIkU5Xnb9C9PA63VmlPwA==",
|
||||
"requires": {
|
||||
"@testing-library/react": "^8.0.7",
|
||||
"classnames": "~2.2.5",
|
||||
"date-fns": "^1.29.0",
|
||||
"eventemitter3": "^3.0.0",
|
||||
"inflection": "~1.12.0",
|
||||
"lodash": "~4.17.5",
|
||||
"prop-types": "^15.6.1",
|
||||
"query-string": "^5.1.1",
|
||||
"recompose": "~0.26.0",
|
||||
"reselect": "~3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ra-language-italian": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ra-language-italian/-/ra-language-italian-3.0.0.tgz",
|
||||
"integrity": "sha512-DUl1BTwYn06ype4ttUmnCfhq0BrrKKx+XuBJkjeIoesKjnT72iccF+/Dq+KUtJhzExVmeWQBA8DzI+0Z3QF+bA=="
|
||||
},
|
||||
"ra-language-portuguese": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/ra-language-portuguese/-/ra-language-portuguese-1.6.0.tgz",
|
||||
"integrity": "sha512-9PAxgrisjmDOTRefjCe2y2ruYQw/iqXnXgUt09vOYUcjY4J0ctabJ4+joGI0jV/x9icF9c7Pui2USc5QDRTktQ=="
|
||||
},
|
||||
"ra-ui-materialui": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ra-ui-materialui/-/ra-ui-materialui-3.3.3.tgz",
|
||||
"integrity": "sha512-qtJH16NQl+ebyNIyrCtYNHiR2IwyZx9XSyRILoJgPdPITiAr+j/cuz7DB6o1D5HQUl5/VOSu4IQIM3jlXjrYFQ==",
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/ra-ui-materialui/-/ra-ui-materialui-3.4.2.tgz",
|
||||
"integrity": "sha512-P1WigQJzIGeMy2jpAoMF0PlkDKZuufUEp/J0y9YFPumHvd5Dvzzz4XCytlL+362XkzgtxVWWH+WlX8CoYD3Y2w==",
|
||||
"requires": {
|
||||
"autosuggest-highlight": "^3.1.1",
|
||||
"classnames": "~2.2.5",
|
||||
@@ -13143,29 +13181,8 @@
|
||||
"prop-types": "^15.7.0",
|
||||
"query-string": "^5.1.1",
|
||||
"react-dropzone": "^10.1.7",
|
||||
"react-transition-group": "^2.2.1",
|
||||
"react-transition-group": "^4.3.0",
|
||||
"recompose": "~0.26.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"dom-helpers": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
|
||||
"integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.1.2"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",
|
||||
"integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==",
|
||||
"requires": {
|
||||
"dom-helpers": "^3.4.0",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"raf": {
|
||||
@@ -13308,9 +13325,9 @@
|
||||
}
|
||||
},
|
||||
"react-admin": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/react-admin/-/react-admin-3.3.3.tgz",
|
||||
"integrity": "sha512-sUiwC/jaL+0RvJFuA/8dsKB7brmno0+d+++Y52G9coBeJceEmY41gEh9Q9w/GUQb4+9VstyJj9Aoq1ns2Qnteg==",
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/react-admin/-/react-admin-3.4.2.tgz",
|
||||
"integrity": "sha512-C0YeuWgd1fskhOk9oG6uyZidw7IEd5MkQWBKahDOY4PW59dZk8hFSuRzkVkaF8B/c/2oM1kCGuVR5likYPrJEA==",
|
||||
"requires": {
|
||||
"@material-ui/core": "^4.3.3",
|
||||
"@material-ui/icons": "^4.2.1",
|
||||
@@ -13318,10 +13335,10 @@
|
||||
"connected-react-router": "^6.5.2",
|
||||
"final-form": "^4.18.5",
|
||||
"final-form-arrays": "^3.0.1",
|
||||
"ra-core": "^3.3.3",
|
||||
"ra-i18n-polyglot": "^3.3.3",
|
||||
"ra-language-english": "^3.2.0",
|
||||
"ra-ui-materialui": "^3.3.3",
|
||||
"ra-core": "^3.4.1",
|
||||
"ra-i18n-polyglot": "^3.4.1",
|
||||
"ra-language-english": "^3.4.1",
|
||||
"ra-ui-materialui": "^3.4.2",
|
||||
"react-final-form": "^6.3.3",
|
||||
"react-final-form-arrays": "^3.1.1",
|
||||
"react-redux": "^7.1.0",
|
||||
@@ -13844,6 +13861,32 @@
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"dom-helpers": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz",
|
||||
"integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^2.6.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz",
|
||||
"integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.5",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
|
||||
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"read-pkg": {
|
||||
@@ -13914,6 +13957,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"indent-string": "^4.0.0",
|
||||
"strip-indent": "^3.0.0"
|
||||
@@ -15390,6 +15434,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"min-indent": "^1.0.0"
|
||||
}
|
||||
|
||||
@@ -3,27 +3,35 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.3.0",
|
||||
"@testing-library/react": "^10.0.1",
|
||||
"@testing-library/user-event": "^10.0.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"md5-hex": "^3.0.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"ra-data-json-server": "^3.3.3",
|
||||
"ra-data-json-server": "^3.4.1",
|
||||
"ra-language-chinese": "^2.0.5",
|
||||
"ra-language-french": "^3.4.1",
|
||||
"ra-language-italian": "^3.0.0",
|
||||
"ra-language-portuguese": "^1.6.0",
|
||||
"react": "^16.13.1",
|
||||
"react-admin": "^3.3.3",
|
||||
"react-admin": "^3.4.2",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-jinke-music-player": "^4.11.2",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-scripts": "^3.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.5.0",
|
||||
"@testing-library/react": "^10.0.2",
|
||||
"@testing-library/user-event": "^10.0.2",
|
||||
"prettier": "^2.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"eject": "react-scripts eject",
|
||||
"prettier": "prettier --write src/**/*.js"
|
||||
},
|
||||
"homepage": ".",
|
||||
"proxy": "http://localhost:4633/",
|
||||
@@ -41,8 +49,5 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^1.19.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Provider } from 'react-redux'
|
||||
import { createHashHistory } from 'history'
|
||||
import { Admin, resolveBrowserLocale, Resource } from 'react-admin'
|
||||
import { Admin, Resource } from 'react-admin'
|
||||
import dataProvider from './dataProvider'
|
||||
import authProvider from './authProvider'
|
||||
import polyglotI18nProvider from 'ra-i18n-polyglot'
|
||||
@@ -21,7 +21,7 @@ import createAdminStore from './store/createAdminStore'
|
||||
|
||||
const i18nProvider = polyglotI18nProvider(
|
||||
(locale) => (messages[locale] ? messages[locale] : messages.en),
|
||||
resolveBrowserLocale()
|
||||
localStorage.getItem('locale') || 'en'
|
||||
)
|
||||
|
||||
const history = createHashHistory()
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
Button,
|
||||
sanitizeListRestProps,
|
||||
TopToolbar,
|
||||
useTranslate
|
||||
useTranslate,
|
||||
} from 'react-admin'
|
||||
import PlayArrowIcon from '@material-ui/icons/PlayArrow'
|
||||
import ShuffleIcon from '@material-ui/icons/Shuffle'
|
||||
@@ -65,5 +65,5 @@ export const AlbumActions = ({
|
||||
|
||||
AlbumActions.defaultProps = {
|
||||
selectedIds: [],
|
||||
onUnselectItems: () => null
|
||||
onUnselectItems: () => null,
|
||||
}
|
||||
|
||||
@@ -3,35 +3,38 @@ import { GridList, GridListTile, GridListTileBar } from '@material-ui/core'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import withWidth from '@material-ui/core/withWidth'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { linkToRecord } from 'ra-core'
|
||||
import { Loading } from 'react-admin'
|
||||
import { linkToRecord, Loading } from 'react-admin'
|
||||
import subsonic from '../subsonic'
|
||||
import { ArtistLinkField } from './ArtistLinkField'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
margin: '20px'
|
||||
margin: '20px',
|
||||
},
|
||||
gridListTile: {
|
||||
minHeight: '180px',
|
||||
minWidth: '180px'
|
||||
minWidth: '180px',
|
||||
},
|
||||
cover: {
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
height: '100%',
|
||||
},
|
||||
tileBar: {
|
||||
textAlign: 'center',
|
||||
background:
|
||||
'linear-gradient(to top, rgba(0,0,0,0.8) 0%,rgba(0,0,0,0.4) 70%,rgba(0,0,0,0) 100%)'
|
||||
'linear-gradient(to top, rgba(0,0,0,1) 0%,rgba(0,0,0,0.4) 70%,rgba(0,0,0,0) 100%)',
|
||||
},
|
||||
albumArtistName: {
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
textAlign: 'center',
|
||||
fontSize: '1em'
|
||||
}
|
||||
fontSize: '1em',
|
||||
},
|
||||
artistLink: {
|
||||
color: theme.palette.primary.light,
|
||||
},
|
||||
}))
|
||||
|
||||
const getColsForWidth = (width) => {
|
||||
@@ -69,7 +72,12 @@ const LoadedAlbumGrid = ({ ids, data, basePath, width }) => {
|
||||
title={data[id].name}
|
||||
subtitle={
|
||||
<div className={classes.albumArtistName}>
|
||||
{data[id].albumArtist}
|
||||
<ArtistLinkField
|
||||
record={data[id]}
|
||||
className={classes.artistLink}
|
||||
>
|
||||
{data[id].albumArtist}
|
||||
</ArtistLinkField>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
NumberInput,
|
||||
ReferenceInput,
|
||||
SearchInput,
|
||||
Pagination
|
||||
Pagination,
|
||||
useTranslate,
|
||||
} from 'react-admin'
|
||||
import { Title } from '../common'
|
||||
import { withWidth } from '@material-ui/core'
|
||||
@@ -17,21 +18,25 @@ import AlbumListView from './AlbumListView'
|
||||
import AlbumGridView from './AlbumGridView'
|
||||
import { ALBUM_MODE_LIST } from './albumState'
|
||||
|
||||
const AlbumFilter = (props) => (
|
||||
<Filter {...props}>
|
||||
<SearchInput source="name" alwaysOn />
|
||||
<ReferenceInput
|
||||
source="artist_id"
|
||||
reference="artist"
|
||||
sort={{ field: 'name', order: 'ASC' }}
|
||||
filterToQuery={(searchText) => ({ name: [searchText] })}
|
||||
>
|
||||
<AutocompleteInput emptyText="-- None --" />
|
||||
</ReferenceInput>
|
||||
<NullableBooleanInput source="compilation" />
|
||||
<NumberInput source="year" />
|
||||
</Filter>
|
||||
)
|
||||
const AlbumFilter = (props) => {
|
||||
const translate = useTranslate()
|
||||
return (
|
||||
<Filter {...props}>
|
||||
<SearchInput source="name" alwaysOn />
|
||||
<ReferenceInput
|
||||
label={translate('resources.album.fields.artist')}
|
||||
source="artist_id"
|
||||
reference="artist"
|
||||
sort={{ field: 'name', order: 'ASC' }}
|
||||
filterToQuery={(searchText) => ({ name: [searchText] })}
|
||||
>
|
||||
<AutocompleteInput emptyText="-- None --" />
|
||||
</ReferenceInput>
|
||||
<NullableBooleanInput source="compilation" />
|
||||
<NumberInput source="year" />
|
||||
</Filter>
|
||||
)
|
||||
}
|
||||
|
||||
const getPerPage = (width) => {
|
||||
if (width === 'xs') return 12
|
||||
|
||||
@@ -35,7 +35,7 @@ const AlbumListActions = ({
|
||||
showFilter,
|
||||
displayedFilters,
|
||||
filterValues,
|
||||
context: 'button'
|
||||
context: 'button',
|
||||
})}
|
||||
<ButtonGroup
|
||||
variant="text"
|
||||
@@ -63,7 +63,7 @@ const AlbumListActions = ({
|
||||
|
||||
AlbumListActions.defaultProps = {
|
||||
selectedIds: [],
|
||||
onUnselectItems: () => null
|
||||
onUnselectItems: () => null,
|
||||
}
|
||||
|
||||
export default AlbumListActions
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
FunctionField,
|
||||
Show,
|
||||
SimpleShowLayout,
|
||||
TextField
|
||||
TextField,
|
||||
} from 'react-admin'
|
||||
import { DurationField, RangeField } from '../common'
|
||||
import { useMediaQuery } from '@material-ui/core'
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ListToolbar,
|
||||
TextField,
|
||||
useListController,
|
||||
DatagridLoading
|
||||
DatagridLoading,
|
||||
} from 'react-admin'
|
||||
import classnames from 'classnames'
|
||||
import { useDispatch } from 'react-redux'
|
||||
@@ -20,7 +20,7 @@ const useStyles = makeStyles(
|
||||
(theme) => ({
|
||||
root: {},
|
||||
main: {
|
||||
display: 'flex'
|
||||
display: 'flex',
|
||||
},
|
||||
content: {
|
||||
marginTop: 0,
|
||||
@@ -28,29 +28,29 @@ const useStyles = makeStyles(
|
||||
position: 'relative',
|
||||
flex: '1 1 auto',
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
boxShadow: 'none'
|
||||
boxShadow: 'none',
|
||||
},
|
||||
overflow: 'inherit'
|
||||
overflow: 'inherit',
|
||||
},
|
||||
bulkActionsDisplayed: {
|
||||
marginTop: -theme.spacing(8),
|
||||
transition: theme.transitions.create('margin-top')
|
||||
transition: theme.transitions.create('margin-top'),
|
||||
},
|
||||
actions: {
|
||||
zIndex: 2,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
flexWrap: 'wrap'
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
noResults: { padding: 20 }
|
||||
noResults: { padding: 20 },
|
||||
}),
|
||||
{ name: 'RaList' }
|
||||
)
|
||||
|
||||
const useStylesListToolbar = makeStyles({
|
||||
toolbar: {
|
||||
justifyContent: 'flex-start'
|
||||
}
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
})
|
||||
|
||||
const trackName = (r) => {
|
||||
@@ -88,7 +88,7 @@ const AlbumSongs = (props) => {
|
||||
<Card
|
||||
className={classnames(classes.content, {
|
||||
[classes.bulkActionsDisplayed]:
|
||||
controllerProps.selectedIds.length > 0
|
||||
controllerProps.selectedIds.length > 0,
|
||||
})}
|
||||
key={version}
|
||||
>
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import { Link } from 'react-admin'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Link } from 'react-admin'
|
||||
|
||||
export const ArtistLinkField = (props) => {
|
||||
const filter = { artist_id: props.record.albumArtistId }
|
||||
export const ArtistLinkField = ({ record, className }) => {
|
||||
const filter = { artist_id: record.albumArtistId }
|
||||
const url = `/album?filter=${JSON.stringify(
|
||||
filter
|
||||
)}&order=ASC&sort=maxYear&displayedFilters={"compilation":true}`
|
||||
return (
|
||||
<Link to={url} onClick={(e) => e.stopPropagation()}>
|
||||
{props.record.albumArtist}
|
||||
<Link to={url} onClick={(e) => e.stopPropagation()} className={className}>
|
||||
{record.albumArtist}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
ArtistLinkField.propTypes = {
|
||||
className: PropTypes.string,
|
||||
source: PropTypes.string,
|
||||
}
|
||||
|
||||
ArtistLinkField.defaultProps = {
|
||||
source: 'artistId',
|
||||
addLabel: true
|
||||
addLabel: true,
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ const albumListParams = {
|
||||
ALBUM_LIST_NEWEST: { sort: { field: 'created_at', order: 'DESC' } },
|
||||
ALBUM_LIST_RECENT: {
|
||||
sort: { field: 'play_date', order: 'DESC' },
|
||||
filter: { starred: true }
|
||||
}
|
||||
filter: { starred: true },
|
||||
},
|
||||
}
|
||||
|
||||
const selectAlbumList = (mode) => ({ type: mode })
|
||||
@@ -24,7 +24,7 @@ const albumViewReducer = (
|
||||
previousState = {
|
||||
mode: ALBUM_MODE_LIST,
|
||||
list: ALBUM_LIST_ALL,
|
||||
params: { sort: {}, filter: {} }
|
||||
params: { sort: {}, filter: {} },
|
||||
},
|
||||
payload
|
||||
) => {
|
||||
@@ -54,5 +54,5 @@ export {
|
||||
ALBUM_LIST_STARRED,
|
||||
albumViewReducer,
|
||||
selectViewMode,
|
||||
selectAlbumList
|
||||
selectAlbumList,
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@ import AlbumShow from './AlbumShow'
|
||||
export default {
|
||||
list: AlbumList,
|
||||
show: AlbumShow,
|
||||
icon: AlbumIcon
|
||||
icon: AlbumIcon,
|
||||
}
|
||||
|
||||
@@ -4,44 +4,44 @@ export const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
padding: '0.7em',
|
||||
minWidth: '24em'
|
||||
minWidth: '24em',
|
||||
},
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
padding: '1em',
|
||||
minWidth: '32em'
|
||||
}
|
||||
minWidth: '32em',
|
||||
},
|
||||
},
|
||||
albumCover: {
|
||||
display: 'inline-block',
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
height: '8em',
|
||||
width: '8em'
|
||||
width: '8em',
|
||||
},
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
height: '10em',
|
||||
width: '10em'
|
||||
width: '10em',
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
height: '15em',
|
||||
width: '15em'
|
||||
}
|
||||
width: '15em',
|
||||
},
|
||||
},
|
||||
albumDetails: {
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'top',
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
width: '14em'
|
||||
width: '14em',
|
||||
},
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: '26em'
|
||||
width: '26em',
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
width: '38em'
|
||||
}
|
||||
width: '38em',
|
||||
},
|
||||
},
|
||||
albumTitle: {
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
List,
|
||||
NumberField,
|
||||
SearchInput,
|
||||
TextField
|
||||
TextField,
|
||||
} from 'react-admin'
|
||||
import { Pagination, Title } from '../common'
|
||||
|
||||
@@ -15,7 +15,7 @@ const ArtistFilter = (props) => (
|
||||
</Filter>
|
||||
)
|
||||
|
||||
const artistRowClick = (id, basePath, record) => {
|
||||
const artistRowClick = (id) => {
|
||||
const filter = { artist_id: id }
|
||||
return `/album?filter=${JSON.stringify(
|
||||
filter
|
||||
|
||||
@@ -3,5 +3,5 @@ import ArtistList from './ArtistList'
|
||||
|
||||
export default {
|
||||
list: ArtistList,
|
||||
icon: MicIcon
|
||||
icon: MicIcon,
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useAuthState, useDataProvider, useTranslate } from 'react-admin'
|
||||
import ReactJkMusicPlayer from 'react-jinke-music-player'
|
||||
import 'react-jinke-music-player/assets/index.css'
|
||||
import subsonic from '../subsonic'
|
||||
import { scrobbled, syncQueue } from './queue'
|
||||
import { scrobble, syncQueue } from './queue'
|
||||
import themes from '../themes'
|
||||
|
||||
const Player = () => {
|
||||
@@ -17,7 +17,7 @@ const Player = () => {
|
||||
theme: playerTheme,
|
||||
bounds: 'body',
|
||||
mode: 'full',
|
||||
autoPlay: true,
|
||||
autoPlay: false,
|
||||
preload: true,
|
||||
autoPlayInitLoadPlayList: true,
|
||||
clearPriorAudioLists: false,
|
||||
@@ -27,27 +27,45 @@ const Player = () => {
|
||||
glassBg: false,
|
||||
showThemeSwitch: false,
|
||||
showMediaSession: true,
|
||||
panelTitle: translate('player.panelTitle'),
|
||||
defaultPosition: {
|
||||
top: 300,
|
||||
left: 120
|
||||
left: 120,
|
||||
},
|
||||
locale: {
|
||||
playListsText: translate('player.playListsText'),
|
||||
openText: translate('player.openText'),
|
||||
closeText: translate('player.closeText'),
|
||||
notContentText: translate('player.notContentText'),
|
||||
clickToPlayText: translate('player.clickToPlayText'),
|
||||
clickToPauseText: translate('player.clickToPauseText'),
|
||||
nextTrackText: translate('player.nextTrackText'),
|
||||
previousTrackText: translate('player.previousTrackText'),
|
||||
reloadText: translate('player.reloadText'),
|
||||
volumeText: translate('player.volumeText'),
|
||||
toggleLyricText: translate('player.toggleLyricText'),
|
||||
toggleMiniModeText: translate('player.toggleMiniModeText'),
|
||||
destroyText: translate('player.destroyText'),
|
||||
downloadText: translate('player.downloadText'),
|
||||
removeAudioListsText: translate('player.removeAudioListsText'),
|
||||
controllerTitle: translate('player.controllerTitle'),
|
||||
clickToDeleteText: (name) =>
|
||||
translate('player.clickToDeleteText', { name }),
|
||||
emptyLyricText: translate('player.emptyLyricText'),
|
||||
playModeText: {
|
||||
order: translate('player.playModeText.order'),
|
||||
orderLoop: translate('player.playModeText.orderLoop'),
|
||||
singleLoop: translate('player.playModeText.singleLoop'),
|
||||
shufflePlay: translate('player.playModeText.shufflePlay')
|
||||
}
|
||||
}
|
||||
shufflePlay: translate('player.playModeText.shufflePlay'),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const addQueueToOptions = (queue) => {
|
||||
return {
|
||||
...defaultOptions,
|
||||
autoPlay: true,
|
||||
autoPlay: queue.playing,
|
||||
clearPriorAudioLists: queue.clear,
|
||||
audioLists: queue.queue.map((item) => item)
|
||||
audioLists: queue.queue.map((item) => item),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +76,7 @@ const Player = () => {
|
||||
const { authenticated } = useAuthState()
|
||||
|
||||
const OnAudioListsChange = (currentPlayIndex, audioLists) => {
|
||||
dispatch(syncQueue(audioLists))
|
||||
dispatch(syncQueue(currentPlayIndex, audioLists))
|
||||
}
|
||||
|
||||
const OnAudioProgress = (info) => {
|
||||
@@ -68,13 +86,14 @@ const Player = () => {
|
||||
}
|
||||
const item = queue.queue.find((item) => item.trackId === info.trackId)
|
||||
if (item && !item.scrobbled) {
|
||||
dispatch(scrobbled(info.trackId))
|
||||
dispatch(scrobble(info.trackId, true))
|
||||
subsonic.scrobble(info.trackId, true)
|
||||
}
|
||||
}
|
||||
|
||||
const OnAudioPlay = (info) => {
|
||||
if (info.duration) {
|
||||
dispatch(scrobble(info.trackId, false))
|
||||
subsonic.scrobble(info.trackId, false)
|
||||
dataProvider.getOne('keepalive', { id: info.trackId })
|
||||
}
|
||||
@@ -90,7 +109,7 @@ const Player = () => {
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <div />
|
||||
return null
|
||||
}
|
||||
|
||||
export default Player
|
||||
|
||||
@@ -13,37 +13,39 @@ const mapToAudioLists = (item) => ({
|
||||
name: item.title,
|
||||
singer: item.artist,
|
||||
cover: subsonic.url('getCoverArt', item.id, { size: 300 }),
|
||||
musicSrc: subsonic.url('stream', item.id, { ts: true })
|
||||
musicSrc: subsonic.url('stream', item.id, { ts: true }),
|
||||
})
|
||||
|
||||
const addTrack = (data) => ({
|
||||
type: PLAYER_ADD_TRACK,
|
||||
data
|
||||
data,
|
||||
})
|
||||
|
||||
const setTrack = (data) => ({
|
||||
type: PLAYER_SET_TRACK,
|
||||
data
|
||||
data,
|
||||
})
|
||||
|
||||
const playAlbum = (id, data) => ({
|
||||
type: PLAYER_PLAY_ALBUM,
|
||||
id,
|
||||
data,
|
||||
id
|
||||
})
|
||||
|
||||
const syncQueue = (data) => ({
|
||||
const syncQueue = (id, data) => ({
|
||||
type: PLAYER_SYNC_QUEUE,
|
||||
data
|
||||
id,
|
||||
data,
|
||||
})
|
||||
|
||||
const scrobbled = (id) => ({
|
||||
const scrobble = (id, submit) => ({
|
||||
type: PLAYER_SCROBBLE,
|
||||
data: id
|
||||
id,
|
||||
submit,
|
||||
})
|
||||
|
||||
const playQueueReducer = (
|
||||
previousState = { queue: [], clear: true },
|
||||
previousState = { queue: [], clear: true, playing: false },
|
||||
payload
|
||||
) => {
|
||||
let queue
|
||||
@@ -52,19 +54,38 @@ const playQueueReducer = (
|
||||
case PLAYER_ADD_TRACK:
|
||||
queue = previousState.queue
|
||||
queue.push(mapToAudioLists(data))
|
||||
return { queue, clear: false }
|
||||
return { ...previousState, queue, clear: false }
|
||||
case PLAYER_SET_TRACK:
|
||||
return { queue: [mapToAudioLists(data)], clear: true }
|
||||
return {
|
||||
...previousState,
|
||||
queue: [mapToAudioLists(data)],
|
||||
clear: true,
|
||||
playing: true,
|
||||
current: data.id,
|
||||
}
|
||||
case PLAYER_SYNC_QUEUE:
|
||||
return { queue: data, clear: false }
|
||||
const currentTrack = data.find((item) => item.id === data.id) || {}
|
||||
return {
|
||||
...previousState,
|
||||
queue: data,
|
||||
clear: false,
|
||||
current: currentTrack.id,
|
||||
}
|
||||
case PLAYER_SCROBBLE:
|
||||
const newQueue = previousState.queue.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
scrobbled: item.scrobbled || item.trackId === data
|
||||
scrobbled:
|
||||
item.scrobbled || (item.trackId === payload.id && payload.submit),
|
||||
}
|
||||
})
|
||||
return { queue: newQueue, clear: false }
|
||||
return {
|
||||
...previousState,
|
||||
queue: newQueue,
|
||||
clear: false,
|
||||
playing: true,
|
||||
current: payload.id,
|
||||
}
|
||||
case PLAYER_PLAY_ALBUM:
|
||||
queue = []
|
||||
let match = false
|
||||
@@ -76,10 +97,16 @@ const playQueueReducer = (
|
||||
queue.push(mapToAudioLists(data[id]))
|
||||
}
|
||||
})
|
||||
return { queue, clear: true }
|
||||
return {
|
||||
...previousState,
|
||||
queue,
|
||||
clear: true,
|
||||
playing: true,
|
||||
current: payload.id,
|
||||
}
|
||||
default:
|
||||
return previousState
|
||||
}
|
||||
}
|
||||
|
||||
export { addTrack, setTrack, playAlbum, syncQueue, scrobbled, playQueueReducer }
|
||||
export { addTrack, setTrack, playAlbum, syncQueue, scrobble, playQueueReducer }
|
||||
|
||||
@@ -8,11 +8,11 @@ const BitrateField = ({ record = {}, source }) => {
|
||||
BitrateField.propTypes = {
|
||||
label: PropTypes.string,
|
||||
record: PropTypes.object,
|
||||
source: PropTypes.string.isRequired
|
||||
source: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
BitrateField.defaultProps = {
|
||||
addLabel: true
|
||||
addLabel: true,
|
||||
}
|
||||
|
||||
export default BitrateField
|
||||
|
||||
@@ -15,11 +15,11 @@ const format = (d) => {
|
||||
DurationField.propTypes = {
|
||||
label: PropTypes.string,
|
||||
record: PropTypes.object,
|
||||
source: PropTypes.string.isRequired
|
||||
source: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
DurationField.defaultProps = {
|
||||
addLabel: true
|
||||
addLabel: true,
|
||||
}
|
||||
|
||||
export default DurationField
|
||||
|
||||
@@ -25,6 +25,6 @@ const PlayButton = ({ icon = defaultIcon, action, ...rest }) => {
|
||||
|
||||
PlayButton.propTypes = {
|
||||
icon: PropTypes.element,
|
||||
action: PropTypes.object
|
||||
action: PropTypes.object,
|
||||
}
|
||||
export default PlayButton
|
||||
|
||||
@@ -22,11 +22,11 @@ const RangeField = ({ record = {}, source }) => {
|
||||
RangeField.propTypes = {
|
||||
label: PropTypes.string,
|
||||
record: PropTypes.object,
|
||||
source: PropTypes.string.isRequired
|
||||
source: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
RangeField.defaultProps = {
|
||||
addLabel: true
|
||||
addLabel: true,
|
||||
}
|
||||
|
||||
export { formatRange }
|
||||
|
||||
@@ -15,9 +15,9 @@ const useStyles = makeStyles(
|
||||
{
|
||||
link: {
|
||||
textDecoration: 'none',
|
||||
color: 'inherit'
|
||||
color: 'inherit',
|
||||
},
|
||||
tertiary: { float: 'right', opacity: 0.541176 }
|
||||
tertiary: { float: 'right', opacity: 0.541176 },
|
||||
},
|
||||
{ name: 'RaSimpleList' }
|
||||
)
|
||||
@@ -28,7 +28,7 @@ const LinkOrNot = ({
|
||||
basePath,
|
||||
id,
|
||||
record,
|
||||
children
|
||||
children,
|
||||
}) => {
|
||||
const classes = useStyles({ classes: classesOverride })
|
||||
return linkType === 'edit' || linkType === true ? (
|
||||
@@ -129,7 +129,7 @@ SimpleList.propTypes = {
|
||||
linkType: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
PropTypes.func
|
||||
PropTypes.func,
|
||||
]).isRequired,
|
||||
onToggleItem: PropTypes.func,
|
||||
primaryText: PropTypes.func,
|
||||
@@ -137,13 +137,13 @@ SimpleList.propTypes = {
|
||||
rightIcon: PropTypes.func,
|
||||
secondaryText: PropTypes.func,
|
||||
selectedIds: PropTypes.arrayOf(PropTypes.any).isRequired,
|
||||
tertiaryText: PropTypes.func
|
||||
tertiaryText: PropTypes.func,
|
||||
}
|
||||
|
||||
SimpleList.defaultProps = {
|
||||
linkType: 'edit',
|
||||
hasBulkActions: false,
|
||||
selectedIds: []
|
||||
selectedIds: [],
|
||||
}
|
||||
|
||||
export default SimpleList
|
||||
|
||||
@@ -20,11 +20,11 @@ function formatBytes(bytes, decimals = 2) {
|
||||
SizeField.propTypes = {
|
||||
label: PropTypes.string,
|
||||
record: PropTypes.object,
|
||||
source: PropTypes.string.isRequired
|
||||
source: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
SizeField.defaultProps = {
|
||||
addLabel: true
|
||||
addLabel: true,
|
||||
}
|
||||
|
||||
export default SizeField
|
||||
|
||||
@@ -20,7 +20,7 @@ const SongDetails = (props) => {
|
||||
bitRate: <BitrateField record={record} source="bitRate" />,
|
||||
size: <SizeField record={record} source="size" />,
|
||||
updatedAt: <DateField record={record} source="updatedAt" showTime />,
|
||||
playCount: <TextField record={record} source="playCount" />
|
||||
playCount: <TextField record={record} source="playCount" />,
|
||||
}
|
||||
if (record.playCount > 0) {
|
||||
data.playDate = <DateField record={record} source="playDate" showTime />
|
||||
@@ -34,7 +34,7 @@ const SongDetails = (props) => {
|
||||
<TableRow key={record.id}>
|
||||
<TableCell component="th" scope="row">
|
||||
{translate(`resources.song.fields.${key}`, {
|
||||
_: inflection.humanize(inflection.underscore(key))
|
||||
_: inflection.humanize(inflection.underscore(key)),
|
||||
})}
|
||||
:
|
||||
</TableCell>
|
||||
|
||||
@@ -18,5 +18,5 @@ export {
|
||||
SimpleList,
|
||||
RangeField,
|
||||
SongDetails,
|
||||
formatRange
|
||||
formatRange,
|
||||
}
|
||||
|
||||
136
ui/src/i18n/cn.js
Normal file
136
ui/src/i18n/cn.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import chineseMessages from 'ra-language-chinese'
|
||||
|
||||
export default deepmerge(chineseMessages, {
|
||||
languageName: '简体中文',
|
||||
resources: {
|
||||
song: {
|
||||
name: '歌曲 |||| 曲库',
|
||||
fields: {
|
||||
title: '标题',
|
||||
artist: '歌手',
|
||||
album: '专辑',
|
||||
path: '路径',
|
||||
genre: '类型',
|
||||
compilation: '收录',
|
||||
albumArtist: '专辑歌手',
|
||||
duration: '时长',
|
||||
year: '年份',
|
||||
playCount: '播放次数',
|
||||
trackNumber: '音轨 #',
|
||||
size: '大小',
|
||||
updatedAt: '上次更新',
|
||||
},
|
||||
bulk: {
|
||||
addToQueue: '稍后播放',
|
||||
},
|
||||
},
|
||||
album: {
|
||||
name: '专辑 |||| 专辑',
|
||||
fields: {
|
||||
name: '名称',
|
||||
albumArtist: '专辑歌手',
|
||||
artist: '歌手',
|
||||
duration: '时长',
|
||||
songCount: '曲目数',
|
||||
playCount: '播放次数',
|
||||
compilation: '合辑',
|
||||
year: '年份',
|
||||
},
|
||||
actions: {
|
||||
playAll: '播放',
|
||||
playNext: '播放下一首',
|
||||
addToQueue: '稍后播放',
|
||||
shuffle: '刷新',
|
||||
},
|
||||
},
|
||||
artist: {
|
||||
name: '歌手 |||| 歌手',
|
||||
fields: {
|
||||
name: '名称',
|
||||
albumCount: '歌手数',
|
||||
},
|
||||
},
|
||||
user: {
|
||||
name: '用户 |||| 用户',
|
||||
fields: {
|
||||
userName: '用户名',
|
||||
isAdmin: '管理员',
|
||||
lastLoginAt: '最后一次访问',
|
||||
updatedAt: '上次修改',
|
||||
name: '名称',
|
||||
},
|
||||
},
|
||||
player: {
|
||||
name: '用户 |||| 用户',
|
||||
fields: {
|
||||
name: '名称',
|
||||
transcodingId: '转码',
|
||||
maxBitRate: '最大比特率',
|
||||
client: '应用程序',
|
||||
userName: '用户',
|
||||
lastSeen: '最后一次访问',
|
||||
},
|
||||
},
|
||||
transcoding: {
|
||||
name: '转码 |||| 转码',
|
||||
fields: {
|
||||
name: '名称',
|
||||
targetFormat: '格式',
|
||||
defaultBitRate: '默认比特率',
|
||||
command: '命令',
|
||||
},
|
||||
},
|
||||
},
|
||||
ra: {
|
||||
auth: {
|
||||
welcome1: '感谢您安装Navidrome!',
|
||||
welcome2: '为了开始使用,请创建一个管理员账户',
|
||||
confirmPassword: '确认密码',
|
||||
buttonCreateAdmin: '创建管理员',
|
||||
},
|
||||
validation: {
|
||||
invalidChars: '请只使用字母和数字',
|
||||
passwordDoesNotMatch: '密码不匹配',
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
library: '曲库',
|
||||
settings: '设置',
|
||||
version: '版本 %{version}',
|
||||
theme: '主题',
|
||||
personal: {
|
||||
name: '个性化',
|
||||
options: {
|
||||
theme: '主题',
|
||||
language: '语言',
|
||||
},
|
||||
},
|
||||
},
|
||||
player: {
|
||||
playListsText: '播放队列',
|
||||
openText: '打开',
|
||||
closeText: '关闭',
|
||||
notContentText: '无音乐',
|
||||
clickToPlayText: '点击播放',
|
||||
clickToPauseText: '点击暂停',
|
||||
nextTrackText: '下一首',
|
||||
previousTrackText: '上一首',
|
||||
reloadText: 'Reload',
|
||||
volumeText: '音量',
|
||||
toggleLyricText: '切换歌词',
|
||||
toggleMiniModeText: '最小化',
|
||||
destroyText: '损坏',
|
||||
downloadText: '下载',
|
||||
removeAudioListsText: '清空播放列表',
|
||||
controllerTitle: '',
|
||||
clickToDeleteText: `点击删除 %{name}`,
|
||||
emptyLyricText: '无歌词',
|
||||
playModeText: {
|
||||
order: '顺序播放',
|
||||
orderLoop: '列表循环',
|
||||
singleLoop: '单曲循环',
|
||||
shufflePlay: '随机播放',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -2,6 +2,7 @@ import deepmerge from 'deepmerge'
|
||||
import englishMessages from 'ra-language-english'
|
||||
|
||||
export default deepmerge(englishMessages, {
|
||||
languageName: 'English',
|
||||
resources: {
|
||||
song: {
|
||||
name: 'Song |||| Songs',
|
||||
@@ -9,53 +10,77 @@ export default deepmerge(englishMessages, {
|
||||
albumArtist: 'Album Artist',
|
||||
duration: 'Time',
|
||||
trackNumber: 'Track #',
|
||||
playCount: 'Plays'
|
||||
playCount: 'Plays',
|
||||
},
|
||||
bulk: {
|
||||
addToQueue: 'Play Later'
|
||||
}
|
||||
addToQueue: 'Play Later',
|
||||
},
|
||||
},
|
||||
album: {
|
||||
fields: {
|
||||
albumArtist: 'Album Artist',
|
||||
artist: 'Artist',
|
||||
duration: 'Time',
|
||||
songCount: 'Songs',
|
||||
playCount: 'Plays'
|
||||
playCount: 'Plays',
|
||||
},
|
||||
actions: {
|
||||
playAll: 'Play',
|
||||
playNext: 'Play Next',
|
||||
addToQueue: 'Play Later',
|
||||
shuffle: 'Shuffle'
|
||||
}
|
||||
}
|
||||
shuffle: 'Shuffle',
|
||||
},
|
||||
},
|
||||
},
|
||||
ra: {
|
||||
auth: {
|
||||
welcome1: 'Thanks for installing Navidrome!',
|
||||
welcome2: 'To start, create an admin user',
|
||||
confirmPassword: 'Confirm Password',
|
||||
buttonCreateAdmin: 'Create Admin'
|
||||
buttonCreateAdmin: 'Create Admin',
|
||||
},
|
||||
validation: {
|
||||
invalidChars: 'Please only use letter and numbers',
|
||||
passwordDoesNotMatch: 'Password does not match'
|
||||
}
|
||||
passwordDoesNotMatch: 'Password does not match',
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
library: 'Library',
|
||||
settings: 'Settings',
|
||||
personal: 'Personal',
|
||||
version: 'Version %{version}',
|
||||
theme: 'Theme'
|
||||
theme: 'Theme',
|
||||
personal: {
|
||||
name: 'Personal',
|
||||
options: {
|
||||
theme: 'Theme',
|
||||
language: 'Language',
|
||||
},
|
||||
},
|
||||
},
|
||||
player: {
|
||||
panelTitle: 'Play Queue',
|
||||
playListsText: 'Play Queue',
|
||||
openText: 'Open',
|
||||
closeText: 'Close',
|
||||
notContentText: 'No music',
|
||||
clickToPlayText: 'Click to play',
|
||||
clickToPauseText: 'Click to pause',
|
||||
nextTrackText: 'Next track',
|
||||
previousTrackText: 'Previous track',
|
||||
reloadText: 'Reload',
|
||||
volumeText: 'Volume',
|
||||
toggleLyricText: 'Toggle lyric',
|
||||
toggleMiniModeText: 'Minimize',
|
||||
destroyText: 'Destroy',
|
||||
downloadText: 'Download',
|
||||
removeAudioListsText: 'Delete audio lists',
|
||||
controllerTitle: '',
|
||||
clickToDeleteText: `Click to delete %{name}`,
|
||||
emptyLyricText: 'No lyric',
|
||||
playModeText: {
|
||||
order: 'In order',
|
||||
orderLoop: 'Repeat',
|
||||
singleLoop: 'Repeat One',
|
||||
shufflePlay: 'Shuffle'
|
||||
}
|
||||
}
|
||||
shufflePlay: 'Shuffle',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
127
ui/src/i18n/fr.js
Normal file
127
ui/src/i18n/fr.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import frenchMessages from 'ra-language-french'
|
||||
|
||||
export default deepmerge(frenchMessages, {
|
||||
languageName: 'Français',
|
||||
resources: {
|
||||
song: {
|
||||
name: 'Piste |||| Pistes',
|
||||
fields: {
|
||||
title: 'Titre',
|
||||
artist: 'Artiste',
|
||||
album: 'Album',
|
||||
path: 'Chemin',
|
||||
genre: 'Genre',
|
||||
compilation: 'Compilation',
|
||||
duration: 'Durée',
|
||||
year: 'Année',
|
||||
playCount: "Nombre d'écoutes",
|
||||
trackNumber: '#',
|
||||
size: 'Taille',
|
||||
updatedAt: 'Mise à jour',
|
||||
},
|
||||
bulk: {
|
||||
addToQueue: 'Ajouter à la file',
|
||||
},
|
||||
},
|
||||
album: {
|
||||
name: 'Album |||| Albums',
|
||||
fields: {
|
||||
name: 'Nom',
|
||||
artist: 'Artiste',
|
||||
songCount: 'Numéro de piste',
|
||||
genre: 'Genre',
|
||||
playCount: "Numbre d'écoutes",
|
||||
compilation: 'Compilation',
|
||||
duration: 'Durée',
|
||||
year: 'Année',
|
||||
},
|
||||
actions: {
|
||||
playAll: 'Lire',
|
||||
playNext: 'Lire ensuite',
|
||||
addToQueue: 'Ajouter à la file',
|
||||
shuffle: 'Mélanger',
|
||||
},
|
||||
},
|
||||
artist: {
|
||||
name: 'Artiste |||| Artistes',
|
||||
fields: {
|
||||
name: 'Nom',
|
||||
albumCount: "Nombre d'albums",
|
||||
},
|
||||
},
|
||||
user: {
|
||||
name: 'Utilisateur |||| Utilisateurs',
|
||||
fields: {
|
||||
userName: 'Nom d\'utilisateur',
|
||||
isAdmin: 'Administrateur',
|
||||
lastLoginAt: 'Dernière connexion',
|
||||
updatedAt: 'Dernière mise à jour',
|
||||
name: 'Nom',
|
||||
},
|
||||
},
|
||||
player: {
|
||||
name: 'Lecteur |||| Lecteurs',
|
||||
fields: {
|
||||
name: 'Nom',
|
||||
transcodingId: 'Transcodage',
|
||||
maxBitRate: 'Bitrate maximum',
|
||||
client: 'Client',
|
||||
userName: 'Nom d\'utilisateur',
|
||||
lastSeen: 'Vu pour la dernière fois',
|
||||
},
|
||||
},
|
||||
transcoding: {
|
||||
name: 'Conversion |||| Conversions',
|
||||
fields: {
|
||||
name: 'Nom',
|
||||
targetFormat: 'Format',
|
||||
defaultBitRate: 'Bitrate par défaut',
|
||||
command: 'Commande',
|
||||
},
|
||||
},
|
||||
},
|
||||
ra: {
|
||||
auth: {
|
||||
welcome1: "Merci d'avoir installé Navidrome !",
|
||||
welcome2: 'Pour commencer, créez un compte administrateur',
|
||||
confirmPassword: 'Confirmer votre mot de passe',
|
||||
buttonCreateAdmin: 'Créer un compte administrateur',
|
||||
},
|
||||
validation: {
|
||||
invalidChars: "Merci d'utiliser uniquement des chiffres et des lettres",
|
||||
passwordDoesNotMatch: 'Les mots de passes ne correspondent pas',
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
library: 'Bibliothèque',
|
||||
settings: 'Paramètres',
|
||||
version: 'Version%{version}',
|
||||
personal: {
|
||||
name: 'Paramètres personel',
|
||||
options: {
|
||||
theme: 'Thème',
|
||||
language: 'Langue',
|
||||
},
|
||||
},
|
||||
},
|
||||
player: {
|
||||
playListsText: 'File de lecture',
|
||||
openText: 'Ouvrir',
|
||||
closeText: 'Fermer',
|
||||
clickToPlayText: 'Cliquer pour lire',
|
||||
clickToPauseText: 'Cliquer pour mettre en pause',
|
||||
nextTrackText: 'Morceau suivant',
|
||||
previousTrackText: 'Morceau précédent',
|
||||
volumeText: 'Volume',
|
||||
toggleMiniModeText: 'Minimiser',
|
||||
removeAudioListsText: 'Vider la liste de lecture',
|
||||
clickToDeleteText: `Cliquer pour supprimer %{name}`,
|
||||
playModeText: {
|
||||
order: 'Ordonner',
|
||||
orderLoop: 'Tout répéter',
|
||||
singleLoop: 'Repéter',
|
||||
shufflePlay: 'Aleatoire',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,3 +1,21 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import en from './en'
|
||||
import fr from './fr'
|
||||
import it from './it'
|
||||
import pt from './pt'
|
||||
import cn from './cn'
|
||||
|
||||
export default { en }
|
||||
const addLanguages = (lang) => {
|
||||
Object.keys(lang).forEach((l) => (languages[l] = deepmerge(en, lang[l])))
|
||||
}
|
||||
const languages = { en }
|
||||
|
||||
// Add new languages to the object bellow (please keep alphabetic sort)
|
||||
addLanguages({ cn, fr, it, pt })
|
||||
|
||||
// "Hack" to make "albumSongs" resource use the same translations as "song"
|
||||
Object.keys(languages).forEach(
|
||||
(k) => (languages[k].resources.albumSong = languages[k].resources.song)
|
||||
)
|
||||
|
||||
export default languages
|
||||
|
||||
127
ui/src/i18n/it.js
Normal file
127
ui/src/i18n/it.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import italianMessages from 'ra-language-italian'
|
||||
|
||||
export default deepmerge(italianMessages, {
|
||||
languageName: 'Italiano',
|
||||
resources: {
|
||||
song: {
|
||||
name: 'Traccia |||| Tracce',
|
||||
fields: {
|
||||
title: 'Titolo',
|
||||
artist: 'Artista',
|
||||
album: 'Album',
|
||||
path: 'Percorso',
|
||||
genre: 'Genere',
|
||||
compilation: 'Compilation',
|
||||
duration: 'Durata',
|
||||
year: 'Anno',
|
||||
playCount: 'Riproduzioni',
|
||||
trackNumber: '#',
|
||||
size: 'Dimensioni',
|
||||
updatedAt: 'Ultimo aggiornamento',
|
||||
},
|
||||
bulk: {
|
||||
addToQueue: 'Aggiungi alla coda',
|
||||
},
|
||||
},
|
||||
album: {
|
||||
name: 'Album |||| Album',
|
||||
fields: {
|
||||
name: 'Nome',
|
||||
artist: 'Artista',
|
||||
songCount: 'Tracce',
|
||||
genre: 'Genere',
|
||||
playCount: 'Riproduzioni',
|
||||
compilation: 'Compilation',
|
||||
duration: 'Durata',
|
||||
year: 'Anno',
|
||||
},
|
||||
actions: {
|
||||
playAll: 'Riproduci',
|
||||
playNext: 'Riproduci come successivo',
|
||||
addToQueue: 'Aggiungi alla coda',
|
||||
shuffle: 'Riprodici casualmente',
|
||||
},
|
||||
},
|
||||
artist: {
|
||||
name: 'Artista |||| Artisti',
|
||||
fields: {
|
||||
name: 'Nome',
|
||||
albumCount: 'Album',
|
||||
},
|
||||
},
|
||||
user: {
|
||||
name: 'Utente |||| Utenti',
|
||||
fields: {
|
||||
userName: 'Utente',
|
||||
isAdmin: 'Amministratore',
|
||||
lastLoginAt: 'Ultimo accesso',
|
||||
updatedAt: 'Ultima modifica',
|
||||
name: 'Nome',
|
||||
},
|
||||
},
|
||||
player: {
|
||||
name: 'Client |||| Client',
|
||||
fields: {
|
||||
name: 'Nome',
|
||||
transcodingId: 'Transcodifica',
|
||||
maxBitRate: 'Bitrate massimo',
|
||||
client: 'Applicazione',
|
||||
userName: 'Utente',
|
||||
lastSeen: 'Ultimo acesso',
|
||||
},
|
||||
},
|
||||
transcoding: {
|
||||
name: 'Transcodifica |||| Transcodifiche',
|
||||
fields: {
|
||||
name: 'Nome',
|
||||
targetFormat: 'Formato',
|
||||
defaultBitRate: 'Bitrate predefinito',
|
||||
command: 'Comando',
|
||||
},
|
||||
},
|
||||
},
|
||||
ra: {
|
||||
auth: {
|
||||
welcome1: 'Grazie per aver installato Navidrome!',
|
||||
welcome2: 'Per iniziare, crea un amministratore',
|
||||
confirmPassword: 'Conferma la password',
|
||||
buttonCreateAdmin: 'Crea amministratore',
|
||||
},
|
||||
validation: {
|
||||
invalidChars: 'Per favore usa solo lettere e numeri',
|
||||
passwordDoesNotMatch: 'Le password non coincidono',
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
library: 'Libreria',
|
||||
settings: 'Impostazioni',
|
||||
version: 'Versione %{version}',
|
||||
personal: {
|
||||
name: 'Personale',
|
||||
options: {
|
||||
theme: 'Tema',
|
||||
language: 'Lingua',
|
||||
},
|
||||
},
|
||||
},
|
||||
player: {
|
||||
playListsText: 'Coda',
|
||||
openText: 'Apri',
|
||||
closeText: 'Chiudi',
|
||||
clickToPlayText: 'Clicca per riprodurre',
|
||||
clickToPauseText: 'Clicca per mettere in pausa',
|
||||
nextTrackText: 'Traccia successiva',
|
||||
previousTrackText: 'Traccia precedente',
|
||||
volumeText: 'Volume',
|
||||
toggleMiniModeText: 'Minimizza',
|
||||
removeAudioListsText: 'Cancella coda',
|
||||
clickToDeleteText: `Clicca per rimuovere %{name}`,
|
||||
playModeText: {
|
||||
order: 'In ordine',
|
||||
orderLoop: 'Ripeti',
|
||||
singleLoop: 'Ripeti una volta',
|
||||
shufflePlay: 'Casuale',
|
||||
},
|
||||
},
|
||||
})
|
||||
127
ui/src/i18n/pt.js
Normal file
127
ui/src/i18n/pt.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import portugueseMessages from 'ra-language-portuguese'
|
||||
|
||||
export default deepmerge(portugueseMessages, {
|
||||
languageName: 'Português',
|
||||
resources: {
|
||||
song: {
|
||||
name: 'Música |||| Músicas',
|
||||
fields: {
|
||||
title: 'Título',
|
||||
artist: 'Artista',
|
||||
album: 'Álbum',
|
||||
path: 'Arquivo',
|
||||
genre: 'Gênero',
|
||||
compilation: 'Coletânea',
|
||||
duration: 'Duração',
|
||||
year: 'Ano',
|
||||
playCount: 'Execuções',
|
||||
trackNumber: '#',
|
||||
size: 'Tamanho',
|
||||
updatedAt: 'Últ. Atualização',
|
||||
},
|
||||
bulk: {
|
||||
addToQueue: 'Play Later',
|
||||
},
|
||||
},
|
||||
album: {
|
||||
name: 'Álbum |||| Álbuns',
|
||||
fields: {
|
||||
name: 'Nome',
|
||||
artist: 'Artista',
|
||||
songCount: 'Músicas',
|
||||
genre: 'Gênero',
|
||||
playCount: 'Execuções',
|
||||
compilation: 'Coletânea',
|
||||
duration: 'Duração',
|
||||
year: 'Ano',
|
||||
},
|
||||
actions: {
|
||||
playAll: 'Play',
|
||||
playNext: 'Play Next',
|
||||
addToQueue: 'Play Later',
|
||||
shuffle: 'Shuffle',
|
||||
},
|
||||
},
|
||||
artist: {
|
||||
name: 'Artista |||| Artistas',
|
||||
fields: {
|
||||
name: 'Nome',
|
||||
albumCount: 'Total de Álbuns',
|
||||
},
|
||||
},
|
||||
user: {
|
||||
name: 'Usuário |||| Usuários',
|
||||
fields: {
|
||||
userName: 'Usuário',
|
||||
isAdmin: 'Admin?',
|
||||
lastLoginAt: 'Últ. Login',
|
||||
updatedAt: 'Últ. Atualização',
|
||||
name: 'Nome',
|
||||
},
|
||||
},
|
||||
player: {
|
||||
name: 'Tocador |||| Tocadores',
|
||||
fields: {
|
||||
name: 'Nome',
|
||||
transcodingId: 'Conversão',
|
||||
maxBitRate: 'Bitrate máx',
|
||||
client: 'Cliente',
|
||||
userName: 'Usuário',
|
||||
lastSeen: 'Últ. acesso',
|
||||
},
|
||||
},
|
||||
transcoding: {
|
||||
name: 'Conversão |||| Conversões',
|
||||
fields: {
|
||||
name: 'Nome',
|
||||
targetFormat: 'Formato',
|
||||
defaultBitRate: 'Bitrate padrão',
|
||||
command: 'Comando',
|
||||
},
|
||||
},
|
||||
},
|
||||
ra: {
|
||||
auth: {
|
||||
welcome1: 'Obrigado por instalar Navidrome!',
|
||||
welcome2: 'Para iniciar, crie um usuário admin',
|
||||
confirmPassword: 'Confirme a senha',
|
||||
buttonCreateAdmin: 'Criar Admin',
|
||||
},
|
||||
validation: {
|
||||
invalidChars: 'Somente use letras e numeros',
|
||||
passwordDoesNotMatch: 'Senha não confere',
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
library: 'Biblioteca',
|
||||
settings: 'Configurações',
|
||||
version: 'Versão %{version}',
|
||||
personal: {
|
||||
name: 'Pessoal',
|
||||
options: {
|
||||
theme: 'Tema',
|
||||
language: 'Língua',
|
||||
},
|
||||
},
|
||||
},
|
||||
player: {
|
||||
playListsText: 'Fila de Execução',
|
||||
openText: 'Abrir',
|
||||
closeText: 'Fechar',
|
||||
clickToPlayText: 'Clique para tocar',
|
||||
clickToPauseText: 'Clique para pausar',
|
||||
nextTrackText: 'Próxima faixa',
|
||||
previousTrackText: 'Faixa anterior',
|
||||
volumeText: 'Volume',
|
||||
toggleMiniModeText: 'Minimizar',
|
||||
removeAudioListsText: 'Limpar fila de execução',
|
||||
clickToDeleteText: `Clique para remover %{name}`,
|
||||
playModeText: {
|
||||
order: 'Em ordem',
|
||||
orderLoop: 'Repetir tudo',
|
||||
singleLoop: 'Repetir',
|
||||
shufflePlay: 'Aleatório',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
AppBar as RAAppBar,
|
||||
MenuItemLink,
|
||||
UserMenu,
|
||||
useTranslate
|
||||
useTranslate,
|
||||
} from 'react-admin'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
import InfoIcon from '@material-ui/icons/Info'
|
||||
@@ -11,8 +11,8 @@ import config from '../config'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
menuItem: {
|
||||
color: theme.palette.text.secondary
|
||||
}
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}))
|
||||
|
||||
const VersionMenu = forwardRef((props, ref) => {
|
||||
@@ -23,7 +23,7 @@ const VersionMenu = forwardRef((props, ref) => {
|
||||
ref={ref}
|
||||
to="#"
|
||||
primaryText={translate('menu.version', {
|
||||
version: config.version
|
||||
version: config.version,
|
||||
})}
|
||||
leftIcon={<InfoIcon />}
|
||||
className={classes.menuItem}
|
||||
|
||||
@@ -7,7 +7,7 @@ import AppBar from './AppBar'
|
||||
import themes from '../themes'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: { paddingBottom: (props) => (props.addPadding ? '80px' : 0) }
|
||||
root: { paddingBottom: (props) => (props.addPadding ? '80px' : 0) },
|
||||
})
|
||||
|
||||
export default (props) => {
|
||||
|
||||
@@ -27,35 +27,35 @@ const useStyles = makeStyles((theme) => ({
|
||||
background: `url(${config.loginBackgroundURL})`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
backgroundPosition: 'center',
|
||||
},
|
||||
card: {
|
||||
minWidth: 300,
|
||||
marginTop: '6em'
|
||||
marginTop: '6em',
|
||||
},
|
||||
avatar: {
|
||||
margin: '1em',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
justifyContent: 'center',
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: theme.palette.secondary.main
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
systemName: {
|
||||
marginTop: '1em',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
color: 'blue' //theme.palette.grey[500]
|
||||
color: 'blue', //theme.palette.grey[500]
|
||||
},
|
||||
form: {
|
||||
padding: '0 1em 1em 1em'
|
||||
padding: '0 1em 1em 1em',
|
||||
},
|
||||
input: {
|
||||
marginTop: '1em'
|
||||
marginTop: '1em',
|
||||
},
|
||||
actions: {
|
||||
padding: '0 1em 1em 1em'
|
||||
}
|
||||
padding: '0 1em 1em 1em',
|
||||
},
|
||||
}))
|
||||
|
||||
const renderInput = ({
|
||||
@@ -274,7 +274,7 @@ const Login = ({ location }) => {
|
||||
|
||||
Login.propTypes = {
|
||||
authProvider: PropTypes.func,
|
||||
previousRoute: PropTypes.string
|
||||
previousRoute: PropTypes.string,
|
||||
}
|
||||
|
||||
// We need to put the ThemeProvider decoration in another component
|
||||
|
||||
@@ -17,9 +17,9 @@ const translatedResourceName = (resource, translate) =>
|
||||
resource.options && resource.options.label
|
||||
? translate(resource.options.label, {
|
||||
smart_count: 2,
|
||||
_: resource.options.label
|
||||
_: resource.options.label,
|
||||
})
|
||||
: inflection.humanize(inflection.pluralize(resource.name))
|
||||
: inflection.humanize(inflection.pluralize(resource.name)),
|
||||
})
|
||||
|
||||
const Menu = ({ onMenuClick, dense, logout }) => {
|
||||
@@ -31,7 +31,7 @@ const Menu = ({ onMenuClick, dense, logout }) => {
|
||||
// TODO State is not persisted in mobile when you close the sidebar menu. Move to redux?
|
||||
const [state, setState] = useState({
|
||||
menuLibrary: true,
|
||||
menuSettings: false
|
||||
menuSettings: false,
|
||||
})
|
||||
|
||||
const handleToggle = (menu) => {
|
||||
|
||||
@@ -5,8 +5,8 @@ import TuneIcon from '@material-ui/icons/Tune'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
menuItem: {
|
||||
color: theme.palette.text.secondary
|
||||
}
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}))
|
||||
|
||||
const PersonalMenu = forwardRef(({ onClick, sidebarIsOpen, dense }, ref) => {
|
||||
@@ -16,7 +16,7 @@ const PersonalMenu = forwardRef(({ onClick, sidebarIsOpen, dense }, ref) => {
|
||||
<MenuItemLink
|
||||
ref={ref}
|
||||
to="/personal"
|
||||
primaryText={translate('menu.personal')}
|
||||
primaryText={translate('menu.personal.name')}
|
||||
leftIcon={<TuneIcon />}
|
||||
onClick={onClick}
|
||||
className={classes.menuItem}
|
||||
|
||||
@@ -14,12 +14,12 @@ const useStyles = makeStyles((theme) => ({
|
||||
icon: { minWidth: theme.spacing(5) },
|
||||
sidebarIsOpen: {
|
||||
paddingLeft: 25,
|
||||
transition: 'padding-left 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms'
|
||||
transition: 'padding-left 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms',
|
||||
},
|
||||
sidebarIsClosed: {
|
||||
paddingLeft: 0,
|
||||
transition: 'padding-left 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms'
|
||||
}
|
||||
transition: 'padding-left 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms',
|
||||
},
|
||||
}))
|
||||
|
||||
const SubMenu = ({
|
||||
@@ -29,7 +29,7 @@ const SubMenu = ({
|
||||
name,
|
||||
icon,
|
||||
children,
|
||||
dense
|
||||
dense,
|
||||
}) => {
|
||||
const translate = useTranslate()
|
||||
const classes = useStyles()
|
||||
|
||||
@@ -1,36 +1,75 @@
|
||||
import React from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Card } from '@material-ui/core'
|
||||
import { Title, SimpleForm, SelectInput, useTranslate } from 'react-admin'
|
||||
import {
|
||||
Title,
|
||||
SimpleForm,
|
||||
SelectInput,
|
||||
useTranslate,
|
||||
useSetLocale,
|
||||
useLocale,
|
||||
} from 'react-admin'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { changeTheme } from './actions'
|
||||
import themes from '../themes'
|
||||
import i18n from '../i18n'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: { marginTop: '1em' }
|
||||
root: { marginTop: '1em' },
|
||||
})
|
||||
|
||||
const Personal = () => {
|
||||
const SelectLanguage = (props) => {
|
||||
const translate = useTranslate()
|
||||
const locale = useLocale()
|
||||
const setLocale = useSetLocale()
|
||||
const langChoices = Object.keys(i18n).map((key) => {
|
||||
return { id: key, name: i18n[key].languageName }
|
||||
})
|
||||
return (
|
||||
<SelectInput
|
||||
{...props}
|
||||
source="lamguage"
|
||||
label={translate('menu.personal.options.language')}
|
||||
defaultValue={locale}
|
||||
choices={langChoices}
|
||||
onChange={(event) => {
|
||||
setLocale(event.target.value)
|
||||
localStorage.setItem('locale', event.target.value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SelectTheme = (props) => {
|
||||
const translate = useTranslate()
|
||||
const classes = useStyles()
|
||||
const currentTheme = useSelector((state) => state.theme)
|
||||
const dispatch = useDispatch()
|
||||
const currentTheme = useSelector((state) => state.theme)
|
||||
const themeChoices = Object.keys(themes).map((key) => {
|
||||
return { id: key, name: themes[key].themeName }
|
||||
})
|
||||
return (
|
||||
<SelectInput
|
||||
{...props}
|
||||
source="theme"
|
||||
label={translate('menu.personal.options.theme')}
|
||||
defaultValue={currentTheme}
|
||||
choices={themeChoices}
|
||||
onChange={(event) => {
|
||||
dispatch(changeTheme(event.target.value))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const Personal = () => {
|
||||
const translate = useTranslate()
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Card className={classes.root}>
|
||||
<Title title={'Navidrome - ' + translate('menu.personal')} />
|
||||
<Title title={'Navidrome - ' + translate('menu.personal.name')} />
|
||||
<SimpleForm toolbar={null}>
|
||||
<SelectInput
|
||||
source="theme"
|
||||
defaultValue={currentTheme}
|
||||
choices={themeChoices}
|
||||
onChange={(event) => {
|
||||
dispatch(changeTheme(event.target.value))
|
||||
}}
|
||||
/>
|
||||
<SelectTheme />
|
||||
<SelectLanguage />
|
||||
</SimpleForm>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -2,5 +2,5 @@ export const CHANGE_THEME = 'CHANGE_THEME'
|
||||
|
||||
export const changeTheme = (theme) => ({
|
||||
type: CHANGE_THEME,
|
||||
payload: theme
|
||||
payload: theme,
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
required,
|
||||
SimpleForm,
|
||||
SelectInput,
|
||||
ReferenceInput
|
||||
ReferenceInput,
|
||||
} from 'react-admin'
|
||||
import { Title } from '../common'
|
||||
|
||||
@@ -19,7 +19,6 @@ const PlayerEdit = (props) => (
|
||||
<SimpleForm>
|
||||
<TextInput source="name" validate={[required()]} />
|
||||
<ReferenceInput
|
||||
label="Transcoding"
|
||||
source="transcodingId"
|
||||
reference="transcoding"
|
||||
sort={{ field: 'name', order: 'ASC' }}
|
||||
@@ -40,7 +39,7 @@ const PlayerEdit = (props) => (
|
||||
{ id: 192, name: '192' },
|
||||
{ id: 256, name: '256' },
|
||||
{ id: 320, name: '320' },
|
||||
{ id: 0, name: 'Unlimited' }
|
||||
{ id: 0, name: 'Unlimited' },
|
||||
]}
|
||||
/>
|
||||
<TextField source="client" />
|
||||
|
||||
@@ -5,29 +5,36 @@ import {
|
||||
TextField,
|
||||
DateField,
|
||||
FunctionField,
|
||||
ReferenceField
|
||||
ReferenceField,
|
||||
} from 'react-admin'
|
||||
import { Title } from '../common'
|
||||
import { useMediaQuery } from '@material-ui/core'
|
||||
import { SimpleList, Title } from '../common'
|
||||
|
||||
const PlayerList = (props) => (
|
||||
<List title={<Title subTitle={'Players'} />} exporter={false} {...props}>
|
||||
<Datagrid rowClick="edit">
|
||||
<TextField source="name" />
|
||||
<ReferenceField
|
||||
label="Transcoding"
|
||||
source="transcodingId"
|
||||
reference="transcoding"
|
||||
>
|
||||
<TextField source="name" />
|
||||
</ReferenceField>
|
||||
<FunctionField
|
||||
label="MaxBitRate"
|
||||
source="maxBitRate"
|
||||
render={(r) => (r.maxBitRate ? r.maxBitRate : 'Unlimited')}
|
||||
/>
|
||||
<DateField source="lastSeen" showTime />
|
||||
</Datagrid>
|
||||
</List>
|
||||
)
|
||||
const PlayerList = (props) => {
|
||||
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
|
||||
return (
|
||||
<List title={<Title subTitle={'Players'} />} exporter={false} {...props}>
|
||||
{isXsmall ? (
|
||||
<SimpleList
|
||||
primaryText={(r) => r.client}
|
||||
secondaryText={(r) => r.userName}
|
||||
tertiaryText={(r) => (r.maxBitRate ? r.maxBitRate : 'Unlimited')}
|
||||
/>
|
||||
) : (
|
||||
<Datagrid rowClick="edit">
|
||||
<TextField source="name" />
|
||||
<ReferenceField source="transcodingId" reference="transcoding">
|
||||
<TextField source="name" />
|
||||
</ReferenceField>
|
||||
<FunctionField
|
||||
source="maxBitRate"
|
||||
render={(r) => (r.maxBitRate ? r.maxBitRate : 'Unlimited')}
|
||||
/>
|
||||
<DateField source="lastSeen" showTime />
|
||||
</Datagrid>
|
||||
)}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayerList
|
||||
|
||||
@@ -5,5 +5,5 @@ import PlayerEdit from './PlayerEdit'
|
||||
export default {
|
||||
list: PlayerList,
|
||||
edit: PlayerEdit,
|
||||
icon: RadioIcon
|
||||
icon: RadioIcon,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
Button,
|
||||
useDataProvider,
|
||||
useTranslate,
|
||||
useUnselectAll
|
||||
useUnselectAll,
|
||||
} from 'react-admin'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { addTrack } from '../audioplayer'
|
||||
|
||||
@@ -11,6 +11,5 @@ export const AlbumLinkField = (props) => (
|
||||
)
|
||||
|
||||
AlbumLinkField.defaultProps = {
|
||||
source: 'albumId',
|
||||
addLabel: true
|
||||
addLabel: true,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
List,
|
||||
NumberField,
|
||||
SearchInput,
|
||||
TextField
|
||||
TextField,
|
||||
} from 'react-admin'
|
||||
import { useMediaQuery } from '@material-ui/core'
|
||||
import {
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Pagination,
|
||||
PlayButton,
|
||||
SimpleList,
|
||||
Title
|
||||
Title,
|
||||
} from '../common'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { addTrack, setTrack } from '../audioplayer'
|
||||
@@ -63,7 +63,7 @@ const SongList = (props) => {
|
||||
rowClick={(id, basePath, record) => dispatch(setTrack(record))}
|
||||
>
|
||||
<TextField source="title" />
|
||||
{isDesktop && <AlbumLinkField source="albumId" sortBy="album" />}
|
||||
{isDesktop && <AlbumLinkField source="album" />}
|
||||
<TextField source="artist" />
|
||||
{isDesktop && <NumberField source="trackNumber" />}
|
||||
{isDesktop && <NumberField source="playCount" />}
|
||||
|
||||
@@ -3,5 +3,5 @@ import SongList from './SongList'
|
||||
|
||||
export default {
|
||||
list: SongList,
|
||||
icon: MusicNoteIcon
|
||||
icon: MusicNoteIcon,
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user