mirror of
https://github.com/miniflux/v2.git
synced 2024-09-27 21:02:41 +02:00
Compare commits
No commits in common. "main" and "2.1.1" have entirely different histories.
@ -1,7 +1,7 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: mcr.microsoft.com/devcontainers/go:1.23
|
image: mcr.microsoft.com/devcontainers/go
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/workspace:cached
|
- ..:/workspace:cached
|
||||||
command: sleep infinity
|
command: sleep infinity
|
||||||
@ -24,7 +24,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
apprise:
|
apprise:
|
||||||
image: caronc/apprise:1.0
|
image: caronc/apprise:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
hostname: apprise
|
hostname: apprise
|
||||||
volumes:
|
volumes:
|
||||||
|
3
.github/pull_request_template.md
vendored
3
.github/pull_request_template.md
vendored
@ -1,7 +1,4 @@
|
|||||||
Do you follow the guidelines?
|
Do you follow the guidelines?
|
||||||
|
|
||||||
- [ ] I have tested my changes
|
- [ ] I have tested my changes
|
||||||
- [ ] There is no breaking changes
|
|
||||||
- [ ] I really tested my changes and there is no regression
|
|
||||||
- [ ] Ideally, my commit messages follow the [Conventional Commits specification](https://www.conventionalcommits.org/)
|
|
||||||
- [ ] I read this document: https://miniflux.app/faq.html#pull-request
|
- [ ] I read this document: https://miniflux.app/faq.html#pull-request
|
||||||
|
3
.github/workflows/build_binaries.yml
vendored
3
.github/workflows/build_binaries.yml
vendored
@ -12,8 +12,7 @@ jobs:
|
|||||||
- name: Set up Golang
|
- name: Set up Golang
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.23.x"
|
go-version: "1.22"
|
||||||
check-latest: true
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Compile binaries
|
- name: Compile binaries
|
||||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.23.x"
|
go-version: "1.22"
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v3
|
||||||
|
2
.github/workflows/debian_packages.yml
vendored
2
.github/workflows/debian_packages.yml
vendored
@ -56,7 +56,7 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
publish-packages:
|
publish-packages:
|
||||||
if: github.event_name == 'push'
|
if: github.event.push
|
||||||
name: Publish Packages
|
name: Publish Packages
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
100
.github/workflows/docker.yml
vendored
100
.github/workflows/docker.yml
vendored
@ -8,8 +8,35 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
jobs:
|
jobs:
|
||||||
docker-images:
|
test-docker-images:
|
||||||
name: Docker Images
|
if: github.event.pull_request
|
||||||
|
name: Test Images
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Build Alpine image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./packaging/docker/alpine/Dockerfile
|
||||||
|
push: false
|
||||||
|
tags: ${{ github.repository_owner }}/miniflux:alpine-dev
|
||||||
|
- name: Test Alpine Docker image
|
||||||
|
run: docker run --rm ${{ github.repository_owner }}/miniflux:alpine-dev miniflux -i
|
||||||
|
- name: Build Distroless image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./packaging/docker/distroless/Dockerfile
|
||||||
|
push: false
|
||||||
|
tags: ${{ github.repository_owner }}/miniflux:distroless-dev
|
||||||
|
- name: Test Distroless Docker image
|
||||||
|
run: docker run --rm ${{ github.repository_owner }}/miniflux:distroless-dev miniflux -i
|
||||||
|
|
||||||
|
publish-docker-images:
|
||||||
|
if: ${{ ! github.event.pull_request }}
|
||||||
|
name: Publish Images
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -19,33 +46,33 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Generate Alpine Docker tags
|
- name: Generate Alpine Docker tag
|
||||||
id: docker_alpine_tags
|
id: docker_alpine_tag
|
||||||
uses: docker/metadata-action@v5
|
run: |
|
||||||
with:
|
DOCKER_IMAGE=${{ github.repository_owner }}/miniflux
|
||||||
images: |
|
DOCKER_VERSION=dev
|
||||||
docker.io/${{ github.repository_owner }}/miniflux
|
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||||
ghcr.io/${{ github.repository_owner }}/miniflux
|
DOCKER_VERSION=nightly
|
||||||
quay.io/${{ github.repository_owner }}/miniflux
|
TAGS="docker.io/${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},quay.io/${DOCKER_IMAGE}:${DOCKER_VERSION}"
|
||||||
tags: |
|
elif [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||||
type=ref,event=pr
|
DOCKER_VERSION=${GITHUB_REF#refs/tags/}
|
||||||
type=schedule,pattern=nightly
|
TAGS="docker.io/${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},quay.io/${DOCKER_IMAGE}:${DOCKER_VERSION},docker.io/${DOCKER_IMAGE}:latest,ghcr.io/${DOCKER_IMAGE}:latest,quay.io/${DOCKER_IMAGE}:latest"
|
||||||
type=semver,pattern={{raw}}
|
fi
|
||||||
|
echo ::set-output name=tags::${TAGS}
|
||||||
|
|
||||||
- name: Generate Distroless Docker tags
|
- name: Generate Distroless Docker tag
|
||||||
id: docker_distroless_tags
|
id: docker_distroless_tag
|
||||||
uses: docker/metadata-action@v5
|
run: |
|
||||||
with:
|
DOCKER_IMAGE=${{ github.repository_owner }}/miniflux
|
||||||
images: |
|
DOCKER_VERSION=dev-distroless
|
||||||
docker.io/${{ github.repository_owner }}/miniflux
|
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||||
ghcr.io/${{ github.repository_owner }}/miniflux
|
DOCKER_VERSION=nightly-distroless
|
||||||
quay.io/${{ github.repository_owner }}/miniflux
|
TAGS="docker.io/${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},quay.io/${DOCKER_IMAGE}:${DOCKER_VERSION}"
|
||||||
tags: |
|
elif [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||||
type=ref,event=pr
|
DOCKER_VERSION=${GITHUB_REF#refs/tags/}-distroless
|
||||||
type=schedule,pattern=nightly
|
TAGS="docker.io/${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},quay.io/${DOCKER_IMAGE}:${DOCKER_VERSION},docker.io/${DOCKER_IMAGE}:latest-distroless,ghcr.io/${DOCKER_IMAGE}:latest-distroless,quay.io/${DOCKER_IMAGE}:latest-distroless"
|
||||||
type=semver,pattern={{raw}}
|
fi
|
||||||
flavor: |
|
echo ::set-output name=tags::${TAGS}
|
||||||
suffix=-distroless,onlatest=true
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@ -54,14 +81,12 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: ${{ github.event_name != 'pull_request' && vars.PUBLISH_DOCKER_IMAGES == 'true' }}
|
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: ${{ github.event_name != 'pull_request' && vars.PUBLISH_DOCKER_IMAGES == 'true' }}
|
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@ -69,7 +94,6 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to Quay Container Registry
|
- name: Login to Quay Container Registry
|
||||||
if: ${{ github.event_name != 'pull_request' && vars.PUBLISH_DOCKER_IMAGES == 'true' }}
|
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
@ -77,21 +101,19 @@ jobs:
|
|||||||
password: ${{ secrets.QUAY_TOKEN }}
|
password: ${{ secrets.QUAY_TOKEN }}
|
||||||
|
|
||||||
- name: Build and Push Alpine images
|
- name: Build and Push Alpine images
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v5
|
||||||
if: ${{ vars.PUBLISH_DOCKER_IMAGES == 'true' }}
|
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./packaging/docker/alpine/Dockerfile
|
file: ./packaging/docker/alpine/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: true
|
||||||
tags: ${{ steps.docker_alpine_tags.outputs.tags }}
|
tags: ${{ steps.docker_alpine_tag.outputs.tags }}
|
||||||
|
|
||||||
- name: Build and Push Distroless images
|
- name: Build and Push Distroless images
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v5
|
||||||
if: ${{ vars.PUBLISH_DOCKER_IMAGES == 'true' }}
|
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./packaging/docker/distroless/Dockerfile
|
file: ./packaging/docker/distroless/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: true
|
||||||
tags: ${{ steps.docker_distroless_tags.outputs.tags }}
|
tags: ${{ steps.docker_distroless_tag.outputs.tags }}
|
||||||
|
20
.github/workflows/linters.yml
vendored
20
.github/workflows/linters.yml
vendored
@ -13,27 +13,25 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install linters
|
- name: Install jshint
|
||||||
run: |
|
run: |
|
||||||
sudo npm install -g jshint@2.13.6 eslint@8.57.0
|
sudo npm install -g jshint@2.13.3
|
||||||
- name: Run jshint
|
- name: Run jshint
|
||||||
run: jshint internal/ui/static/js/*.js
|
run: jshint ui/static/js/*.js
|
||||||
- name: Run ESLint
|
|
||||||
run: eslint internal/ui/static/js/*.js
|
|
||||||
|
|
||||||
golangci:
|
golangci:
|
||||||
name: Golang Linters
|
name: Golang Linter
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.23.x"
|
go-version: "1.22"
|
||||||
- run: "go vet ./..."
|
- run: "go vet ./..."
|
||||||
- uses: golangci/golangci-lint-action@v6
|
- uses: golangci/golangci-lint-action@v4
|
||||||
with:
|
with:
|
||||||
args: --timeout 10m --skip-dirs tests --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace --enable gocritic
|
args: --timeout 10m --skip-dirs tests --disable errcheck --enable sqlclosecheck --enable misspell --enable gofmt --enable goimports --enable whitespace
|
||||||
- uses: dominikh/staticcheck-action@v1.3.1
|
- uses: dominikh/staticcheck-action@v1.3.0
|
||||||
with:
|
with:
|
||||||
version: "2024.1.1"
|
version: "2023.1.7"
|
||||||
install-go: false
|
install-go: false
|
||||||
|
2
.github/workflows/rpm_packages.yml
vendored
2
.github/workflows/rpm_packages.yml
vendored
@ -38,7 +38,7 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
publish-package:
|
publish-package:
|
||||||
if: github.event_name == 'push'
|
if: github.event.push
|
||||||
name: Publish Packages
|
name: Publish Packages
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
|||||||
max-parallel: 4
|
max-parallel: 4
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||||
go-version: ["1.23.x"]
|
go-version: ["1.22"]
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.23.x"
|
go-version: "1.22"
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Install Postgres client
|
- name: Install Postgres client
|
||||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,7 +1,6 @@
|
|||||||
./*.sha256
|
miniflux-*
|
||||||
./miniflux
|
./miniflux
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
*.deb
|
|
||||||
*.rpm
|
*.rpm
|
||||||
miniflux-*
|
*.deb
|
||||||
|
.idea
|
||||||
|
.vscode
|
191
ChangeLog
191
ChangeLog
@ -1,194 +1,3 @@
|
|||||||
Version 2.2.0 (August 18, 2024)
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
* refactor: simplify Youtube feeds discovery
|
|
||||||
* fix(integration): define content encoding explicitly when sending article body to Readeck
|
|
||||||
* fix(fever): correct sorting direction when using `max_id` argument
|
|
||||||
* fix(client): Return `nil` and error if endpoint is an empty string
|
|
||||||
* fix: video poster image URL is encoded twice when using `MEDIA_PROXY_MODE=all`
|
|
||||||
* fix: use `BASE_URL` instead of `r.Host` to generate absolute media proxy URL
|
|
||||||
* fix: panic during YouTube channel feed discovery
|
|
||||||
* fix: honor `hide_globally` when creating a new feed through the api
|
|
||||||
* fix: align pagination correctly on small screens with non-English text
|
|
||||||
* fix: `store.GetEnclosure()` should return `nil` if no rows are returned
|
|
||||||
* feat(locale): update Turkish translations
|
|
||||||
* feat(locale): update French translations
|
|
||||||
* feat(locale): update Chinese` translations
|
|
||||||
* feat(integration): add ntfy integration
|
|
||||||
* feat(api): add API routes `/v1/enclosures/{enclosureID}`
|
|
||||||
* feat: validate `OAUTH2_PROVIDER` config option value
|
|
||||||
* feat: remove YouTube video page subscription finder because `meta[itemprop="channelId"]` no longer exists
|
|
||||||
* feat: remove well-known URL parameter trackers
|
|
||||||
* feat: mark media as read when playback reaches 90%
|
|
||||||
* feat: change log level to info when running migrations
|
|
||||||
* feat: allow customizing the display name of the OpenID Connect provider
|
|
||||||
* feat: add support for `base` HTML element when discovering feeds
|
|
||||||
* feat: add support for `aside` HTML element in entry content
|
|
||||||
* feat: Add option to disable local auth form
|
|
||||||
* feat: add license info to Javascript files for LibreJS compatibility
|
|
||||||
* feat: add `FETCH_BILIBILI_WATCH_TIME` config option
|
|
||||||
* docs: update links to filtering rules
|
|
||||||
* chore: avoid using legacy key/value format in Dockerfile
|
|
||||||
* build(deps): bump `golang.org/x/oauth2` from `0.21.0` to `0.22.0`
|
|
||||||
* build(deps): bump `golang.org/x/net` from `0.27.0` to `0.28.0`
|
|
||||||
* build(deps): bump `golang.org/x/crypto` from `0.25.0` to `0.26.0`
|
|
||||||
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.20.36` to `2.20.37`
|
|
||||||
* build(deps): bump `github.com/prometheus/client_golang`
|
|
||||||
* build: update GitHub Actions to Go 1.23
|
|
||||||
* build: publish OCI images only if `PUBLISH_DOCKER_IMAGES=true`
|
|
||||||
* build: bump Alpine Linux build image to v3.20
|
|
||||||
* build: add sha256 checksum file for published binaries
|
|
||||||
|
|
||||||
Version 2.1.4 (July 9, 2024)
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
* test: add unit tests for `IsModified()` behaviour
|
|
||||||
* refactor: improve YouTube page feed detection
|
|
||||||
* fix(ui): settings form is not populated correctly after validation errors
|
|
||||||
* fix(ui): playback speed indicator precision
|
|
||||||
* fix(ui): playback speed indicator on shared entries
|
|
||||||
* fix(integration): preserve existing Pinboard bookmarks
|
|
||||||
* fix(googlereader): set `CrawlTimeMsec` to the correct precision
|
|
||||||
* fix(build): failed to solve container image `arm64v8/golang:1.22-bookworm`
|
|
||||||
* fix(build): add `distroless` suffix on `latest` tag in GitHub workflow
|
|
||||||
* fix: use `ETag` as a stronger validator than `Last-Modified`
|
|
||||||
* fix: update `theverge.com` rewrite rule to avoid duplicate image
|
|
||||||
* fix: incorrect Go package comment `reader/readingtime`
|
|
||||||
* fix: error out for improper rewrite regexp when processing feed entries
|
|
||||||
* fix: ensures that session cookies are not expiring before the session is cleaned up from the database as per `CLEANUP_REMOVE_SESSIONS_DAYS`
|
|
||||||
* fix: `<img>` aspect ratio with `height: auto`
|
|
||||||
* feat(ui): add `viewport-fit=cover`
|
|
||||||
* feat(sanitizer): add support for HTML hidden attribute
|
|
||||||
* feat(locale): update French translations
|
|
||||||
* feat(integration): add Raindrop integration
|
|
||||||
* feat(integration): add feed name to Telegram message
|
|
||||||
* feat(integration): add Betula integration
|
|
||||||
* feat: use of insecure TLS ciphers when "Allow self-signed or invalid certificates" is enabled to workaround some broken websites
|
|
||||||
* feat: discover feeds from a Youtube playlist pages
|
|
||||||
* feat: add navigation to last/first page
|
|
||||||
* feat: add global block and keep filters
|
|
||||||
* feat: add description field to feed settings
|
|
||||||
* feat: add `pitchfork.com` scraping rule
|
|
||||||
* feat: add `FETCH_NEBULA_WATCH_TIME` config option
|
|
||||||
* Bump `github.com/PuerkitoBio/goquery` from` 1.9.1` to` 1.9.2`
|
|
||||||
* Bump `github.com/prometheus/client_golang` from `1.19.0` to `1.19.1`
|
|
||||||
* build(deps): bump `library/alpine` in `/packaging/docker/alpine`
|
|
||||||
* build(deps): bump `golangci/golangci-lint-action` from `4` to `6`
|
|
||||||
* build(deps): bump `golang.org/x/term` from `0.19.0` to `0.22.0`
|
|
||||||
* build(deps): bump `golang.org/x/oauth2` from `0.19.0` to `0.21.0`
|
|
||||||
* build(deps): bump `golang.org/x/net` from `0.22.0` to `0.27.0`
|
|
||||||
* build(deps): bump `golang.org/x/crypto` from `0.24.0` to `0.25.0`
|
|
||||||
* build(deps): bump `github.com/yuin/goldmark` from `1.7.1` to `1.7.4`
|
|
||||||
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.20.20` to `2.20.36`
|
|
||||||
* build(deps): bump `github.com/coreos/go-oidc/v3` from `3.10.0` to `3.11.0`
|
|
||||||
* build(deps): bump `docker/build-push-action` from `5` to `6`
|
|
||||||
|
|
||||||
Version 2.1.3 (April 27, 2024)
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
* `api`: `rand.Intn(math.MaxInt64)` causes tests to fail on 32-bit architectures (use `rand.Int()` instead)
|
|
||||||
* `ci`: use `docker/metadata-action` instead of deprecated shell-scripts
|
|
||||||
* `database`: remove `entries_feed_url_idx` index because entry URLs can exceeds btree index size limit
|
|
||||||
* `finder`: find feeds from YouTube playlist
|
|
||||||
* `http/response`: add brotli compression support
|
|
||||||
* `integration/matrix`: fix function name in comment
|
|
||||||
* `packaging`: specify container registry explicitly (e.g., Podman does not use `docker.io` by default)
|
|
||||||
* `packaging`: use `make miniflux` instead of duplicating `go build` arguments (this leverages Go's PIE build mode)
|
|
||||||
* `reader/fetcher`: add brotli content encoding support
|
|
||||||
* `reader/processor`: minimize feed entries HTML content
|
|
||||||
* `reader/rewrite`: add a rule for `oglaf.com`
|
|
||||||
* `storage`: change `GetReadTime()` function to use `entries_feed_id_hash_key` index
|
|
||||||
* `ui`: add seek and speed controls to media player
|
|
||||||
* `ui`: add tag entries page
|
|
||||||
* `ui`: fix JavaScript error when clicking on unread counter
|
|
||||||
* `ui`: use `FORCE_REFRESH_INTERVAL` config for category refresh
|
|
||||||
* Bump `github.com/tdewolff/minify/v2` from `2.20.19` to `2.20.20`
|
|
||||||
* Bump `golang.org/x/net` from `0.22.0` to `0.24.0`
|
|
||||||
* Bump `golang.org/x/term` from `0.18.0` to `0.19.0`
|
|
||||||
* Bump `golang.org/x/oauth2` from `0.18.0` to `0.19.0`
|
|
||||||
* Bump `github.com/yuin/goldmark` from `1.7.0` to `1.7.1`
|
|
||||||
|
|
||||||
Version 2.1.2 (March 30, 2024)
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
* `api`: rewrite API integration tests without build tags
|
|
||||||
* `ci`: add basic ESLinter checks
|
|
||||||
* `ci`: enable go-critic linter and fix various issues detected
|
|
||||||
* `ci`: fix JavaScript linter path in GitHub Actions
|
|
||||||
* `cli`: avoid misleading error message when creating an admin user automatically
|
|
||||||
* `config`: add `FILTER_ENTRY_MAX_AGE_DAYS` option
|
|
||||||
* `config`: bump the number of simultaneous workers
|
|
||||||
* `config`: rename `PROXY_*` options to `MEDIA_PROXY_*`
|
|
||||||
* `config`: use `crypto.GenerateRandomBytes` instead of doing it by hand
|
|
||||||
* `http/request`: refactor conditions to be more idiomatic
|
|
||||||
* `http/response`: remove legacy `X-XSS-Protection` header
|
|
||||||
* `integration/rssbrige`: fix rssbrige import
|
|
||||||
* `integration/shaarli`: factorize the header+payload concatenation as data
|
|
||||||
* `integration/shaarli`: no need to base64-encode then remove the padding when we can simply encode without padding
|
|
||||||
* `integration/shaarli`: the JWT token was declared as using HS256 as algorithm, but was using HS512
|
|
||||||
* `integration/webhook`: add category title to request body
|
|
||||||
* `locale`: update Turkish translations
|
|
||||||
* `man page`: sort config options in alphabetical order
|
|
||||||
* `mediaproxy`: reduce the internal indentation of `ProxifiedUrl` by inverting some conditions
|
|
||||||
* `mediaproxy`: simplify and refactor the package
|
|
||||||
* `model`: replace` Optional{Int,Int64,Float64}` with a generic function `OptionalNumber()`
|
|
||||||
* `model`: use struct embedding for `FeedCreationRequestFromSubscriptionDiscovery` to reduce code duplication
|
|
||||||
* `reader/atom`: avoid debug message when the date is empty
|
|
||||||
* `reader/atom`: change `if !a { a = } if !a {a = }` constructs into `if !a { a = ; if !a {a = }}` to reduce the number of comparisons and improve readability
|
|
||||||
* `reader/atom`: Move the population of the feed's entries into a new function, to make BuildFeed easier to understand/separate concerns/implementation details
|
|
||||||
* `reader/atom`: refactor Atom parser to use an adapter
|
|
||||||
* `reader/atom`: use `sort+compact` instead of `compact+sort` to remove duplicates
|
|
||||||
* `reader/atom`: when detecting the format, detect its version as well
|
|
||||||
* `reader/encoding`: inline a one-liner function
|
|
||||||
* `reader/handler`: fix force refresh feature
|
|
||||||
* `reader/json`: refactor JSON Feed parser to use an adapter
|
|
||||||
* `reader/media`: remove a superfluous error-check: `strconv.ParseInt` returns `0` when passed an empty string
|
|
||||||
* `reader/media`: simplify switch-case by moving a common condition above it
|
|
||||||
* `reader/processor`: compile block/keep regex only once per feed
|
|
||||||
* `reader/rdf`: refactor RDF parser to use an adapter
|
|
||||||
* `reader/rewrite`: inline some one-line functions
|
|
||||||
* `reader/rewrite`: simplify `removeClickbait`
|
|
||||||
* `reader/rewrite`: transform a free-standing function into a method
|
|
||||||
* `reader/rewrite`: use a proper constant instead of a magic number in `applyFuncOnTextContent`
|
|
||||||
* `reader/rss`: add support for `<media:category>` element
|
|
||||||
* `reader/rss`: don't add empty tags to RSS items
|
|
||||||
* `reader/rss`: refactor RSS parser to use a default namespace to avoid some limitations of the Go XML parser
|
|
||||||
* `reader/rss`: refactor RSS Parser to use an adapter
|
|
||||||
* `reader/rss`: remove some duplicated code in RSS parser
|
|
||||||
* `reader`: ensure that enclosure URLs are always absolute
|
|
||||||
* `reader`: move iTunes and GooglePlay XML definitions to their own packages
|
|
||||||
* `reader`: parse podcast categories
|
|
||||||
* `reader`: remove trailing space in `SiteURL` and `FeedURL`
|
|
||||||
* `storage`: do not store empty tags
|
|
||||||
* `storage`: simplify `removeDuplicates()` to use a `sort`+`compact` construct instead of doing it by hand with a hashmap
|
|
||||||
* `storage`: Use plain strings concatenation instead of building an array and then joining it
|
|
||||||
* `timezone`: make sure the tests pass when the timezone database is not installed on the host
|
|
||||||
* `ui/css`: align `min-width` with the other `min-width` values
|
|
||||||
* `ui/css`: fix regression: "Add to Home Screen" button is unreadable
|
|
||||||
* `ui/js`: don't use lambdas to return a function, use directly the function instead
|
|
||||||
* `ui/js`: enable trusted-types
|
|
||||||
* `ui/js`: fix download button loading label
|
|
||||||
* `ui/js`: fix JavaScript error on the login page when the user not authenticated
|
|
||||||
* `ui/js`: inline one-line functions
|
|
||||||
* `ui/js`: inline some `querySelectorAll` calls
|
|
||||||
* `ui/js`: reduce the scope of some variables
|
|
||||||
* `ui/js`: remove a hack for "Chrome 67 and earlier" since it was released in 2018
|
|
||||||
* `ui/js`: replace `DomHelper.findParent` with `.closest`
|
|
||||||
* `ui/js`: replace `let` with `const`
|
|
||||||
* `ui/js`: simplify `DomHelper.getVisibleElements` by using a `filter` instead of a loop with an index
|
|
||||||
* `ui/js`: use a `Set` instead of an array in a `KeyboardHandler`'s member
|
|
||||||
* `ui/js`: use some ternaries where it makes sense
|
|
||||||
* `ui/static`: make use of `HashFromBytes` everywhere
|
|
||||||
* `ui/static`: set minifier ECMAScript version
|
|
||||||
* `ui`: add keyboard shortcuts for scrolling to top/bottom of the item list
|
|
||||||
* `ui`: add media player control playback speed
|
|
||||||
* `ui`: remove unused variables and improve JSON decoding in `saveEnclosureProgression()`
|
|
||||||
* `validator`: display an error message on edit feed page when the feed URL is not unique
|
|
||||||
* Bump `github.com/coreos/go-oidc/v3` from `3.9.0` to `3.10.0`
|
|
||||||
* Bump `github.com/go-webauthn/webauthn` from `0.10.1` to `0.10.2`
|
|
||||||
* Bump `github.com/tdewolff/minify/v2` from `2.20.18` to `2.20.19`
|
|
||||||
* Bump `google.golang.org/protobuf` from `1.32.0` to `1.33.0`
|
|
||||||
|
|
||||||
Version 2.1.1 (March 10, 2024)
|
Version 2.1.1 (March 10, 2024)
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
|
54
Makefile
54
Makefile
@ -1,12 +1,12 @@
|
|||||||
APP := miniflux
|
APP := miniflux
|
||||||
DOCKER_IMAGE := miniflux/miniflux
|
DOCKER_IMAGE := miniflux/miniflux
|
||||||
VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null)
|
VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null)
|
||||||
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
|
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null)
|
||||||
BUILD_DATE := `date +%FT%T%z`
|
BUILD_DATE := `date +%FT%T%z`
|
||||||
LD_FLAGS := "-s -w -X 'miniflux.app/v2/internal/version.Version=$(VERSION)' -X 'miniflux.app/v2/internal/version.Commit=$(COMMIT)' -X 'miniflux.app/v2/internal/version.BuildDate=$(BUILD_DATE)'"
|
LD_FLAGS := "-s -w -X 'miniflux.app/v2/internal/version.Version=$(VERSION)' -X 'miniflux.app/v2/internal/version.Commit=$(COMMIT)' -X 'miniflux.app/v2/internal/version.BuildDate=$(BUILD_DATE)'"
|
||||||
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
|
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
|
||||||
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
|
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
|
||||||
DOCKER_PLATFORM := amd64
|
DEB_IMG_ARCH := amd64
|
||||||
|
|
||||||
export PGPASSWORD := postgres
|
export PGPASSWORD := postgres
|
||||||
|
|
||||||
@ -51,43 +51,33 @@ miniflux-no-pie:
|
|||||||
|
|
||||||
linux-amd64:
|
linux-amd64:
|
||||||
@ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
@ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
|
||||||
|
|
||||||
linux-arm64:
|
linux-arm64:
|
||||||
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
|
||||||
|
|
||||||
linux-armv7:
|
linux-armv7:
|
||||||
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
|
||||||
|
|
||||||
linux-armv6:
|
linux-armv6:
|
||||||
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
|
||||||
|
|
||||||
linux-armv5:
|
linux-armv5:
|
||||||
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
@ CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=5 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
|
||||||
|
|
||||||
darwin-amd64:
|
darwin-amd64:
|
||||||
@ GOOS=darwin GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
@ GOOS=darwin GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
|
||||||
|
|
||||||
darwin-arm64:
|
darwin-arm64:
|
||||||
@ GOOS=darwin GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
@ GOOS=darwin GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
|
||||||
|
|
||||||
freebsd-amd64:
|
freebsd-amd64:
|
||||||
@ CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
@ CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
|
||||||
|
|
||||||
openbsd-amd64:
|
openbsd-amd64:
|
||||||
@ GOOS=openbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
@ GOOS=openbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@ main.go
|
||||||
@ sha256sum $(APP)-$@ > $(APP)-$@.sha256
|
|
||||||
|
|
||||||
windows-amd64:
|
windows-amd64:
|
||||||
@ GOOS=windows GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@.exe main.go
|
@ GOOS=windows GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@.exe main.go
|
||||||
@ sha256sum $(APP)-$@.exe > $(APP)-$@.exe.sha256
|
|
||||||
|
|
||||||
build: linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-armv5 darwin-amd64 darwin-arm64 freebsd-amd64 openbsd-amd64 windows-amd64
|
build: linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-armv5 darwin-amd64 darwin-arm64 freebsd-amd64 openbsd-amd64 windows-amd64
|
||||||
|
|
||||||
@ -111,10 +101,10 @@ windows-x86:
|
|||||||
@ GOOS=windows GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@.exe main.go
|
@ GOOS=windows GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-$@.exe main.go
|
||||||
|
|
||||||
run:
|
run:
|
||||||
@ LOG_DATE_TIME=1 LOG_LEVEL=debug RUN_MIGRATIONS=1 CREATE_ADMIN=1 ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run main.go
|
@ LOG_DATE_TIME=1 DEBUG=1 RUN_MIGRATIONS=1 CREATE_ADMIN=1 ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run main.go
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb $(APP)*.exe $(APP)*.sha256
|
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb $(APP)*.exe
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test -cover -race -count=1 ./...
|
go test -cover -race -count=1 ./...
|
||||||
@ -138,11 +128,7 @@ integration-test:
|
|||||||
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
|
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
|
||||||
|
|
||||||
while ! nc -z localhost 8080; do sleep 1; done
|
while ! nc -z localhost 8080; do sleep 1; done
|
||||||
|
go test -v -tags=integration -count=1 miniflux.app/v2/internal/tests
|
||||||
TEST_MINIFLUX_BASE_URL=http://127.0.0.1:8080 \
|
|
||||||
TEST_MINIFLUX_ADMIN_USERNAME=admin \
|
|
||||||
TEST_MINIFLUX_ADMIN_PASSWORD=test123 \
|
|
||||||
go test -v -count=1 ./internal/api
|
|
||||||
|
|
||||||
clean-integration-test:
|
clean-integration-test:
|
||||||
@ kill -9 `cat /tmp/miniflux.pid`
|
@ kill -9 `cat /tmp/miniflux.pid`
|
||||||
@ -173,15 +159,15 @@ rpm: clean
|
|||||||
rpmbuild -bb --define "_miniflux_version $(VERSION)" /root/rpmbuild/SPECS/miniflux.spec
|
rpmbuild -bb --define "_miniflux_version $(VERSION)" /root/rpmbuild/SPECS/miniflux.spec
|
||||||
|
|
||||||
debian:
|
debian:
|
||||||
@ docker buildx build --load \
|
@ docker build --load \
|
||||||
--platform linux/$(DOCKER_PLATFORM) \
|
--build-arg BASE_IMAGE_ARCH=$(DEB_IMG_ARCH) \
|
||||||
-t miniflux-deb-builder \
|
-t $(DEB_IMG_ARCH)/miniflux-deb-builder \
|
||||||
-f packaging/debian/Dockerfile \
|
-f packaging/debian/Dockerfile \
|
||||||
.
|
.
|
||||||
@ docker run --rm --platform linux/$(DOCKER_PLATFORM) \
|
@ docker run --rm \
|
||||||
-v ${PWD}:/pkg miniflux-deb-builder
|
-v ${PWD}:/pkg $(DEB_IMG_ARCH)/miniflux-deb-builder
|
||||||
|
|
||||||
debian-packages: clean
|
debian-packages: clean
|
||||||
$(MAKE) debian DOCKER_PLATFORM=amd64
|
$(MAKE) debian DEB_IMG_ARCH=amd64
|
||||||
$(MAKE) debian DOCKER_PLATFORM=arm64
|
$(MAKE) debian DEB_IMG_ARCH=arm64v8
|
||||||
$(MAKE) debian DOCKER_PLATFORM=arm/v7
|
$(MAKE) debian DEB_IMG_ARCH=arm32v7
|
||||||
|
@ -18,44 +18,16 @@ type Client struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new Miniflux client.
|
// New returns a new Miniflux client.
|
||||||
// Deprecated: use NewClient instead.
|
|
||||||
func New(endpoint string, credentials ...string) *Client {
|
func New(endpoint string, credentials ...string) *Client {
|
||||||
return NewClient(endpoint, credentials...)
|
// Web gives "API Endpoint = https://miniflux.app/v1/", it doesn't work (/v1/v1/me)
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient returns a new Miniflux client.
|
|
||||||
func NewClient(endpoint string, credentials ...string) *Client {
|
|
||||||
// Trim trailing slashes and /v1 from the endpoint.
|
|
||||||
endpoint = strings.TrimSuffix(endpoint, "/")
|
endpoint = strings.TrimSuffix(endpoint, "/")
|
||||||
endpoint = strings.TrimSuffix(endpoint, "/v1")
|
endpoint = strings.TrimSuffix(endpoint, "/v1")
|
||||||
switch len(credentials) {
|
// trim to https://miniflux.app
|
||||||
case 2:
|
|
||||||
|
if len(credentials) == 2 {
|
||||||
return &Client{request: &request{endpoint: endpoint, username: credentials[0], password: credentials[1]}}
|
return &Client{request: &request{endpoint: endpoint, username: credentials[0], password: credentials[1]}}
|
||||||
case 1:
|
|
||||||
return &Client{request: &request{endpoint: endpoint, apiKey: credentials[0]}}
|
|
||||||
default:
|
|
||||||
return &Client{request: &request{endpoint: endpoint}}
|
|
||||||
}
|
}
|
||||||
}
|
return &Client{request: &request{endpoint: endpoint, apiKey: credentials[0]}}
|
||||||
|
|
||||||
// Healthcheck checks if the application is up and running.
|
|
||||||
func (c *Client) Healthcheck() error {
|
|
||||||
body, err := c.request.Get("/healthcheck")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("miniflux: unable to perform healthcheck: %w", err)
|
|
||||||
}
|
|
||||||
defer body.Close()
|
|
||||||
|
|
||||||
responseBodyContent, err := io.ReadAll(body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("miniflux: unable to read healthcheck response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(responseBodyContent) != "OK" {
|
|
||||||
return fmt.Errorf("miniflux: invalid healthcheck response: %q", responseBodyContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version returns the version of the Miniflux instance.
|
// Version returns the version of the Miniflux instance.
|
||||||
@ -556,25 +528,6 @@ func (c *Client) SaveEntry(entryID int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchEntryOriginalContent fetches the original content of an entry using the scraper.
|
|
||||||
func (c *Client) FetchEntryOriginalContent(entryID int64) (string, error) {
|
|
||||||
body, err := c.request.Get(fmt.Sprintf("/v1/entries/%d/fetch-content", entryID))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer body.Close()
|
|
||||||
|
|
||||||
var response struct {
|
|
||||||
Content string `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewDecoder(body).Decode(&response); err != nil {
|
|
||||||
return "", fmt.Errorf("miniflux: response error (%v)", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Content, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchCounters fetches feed counters.
|
// FetchCounters fetches feed counters.
|
||||||
func (c *Client) FetchCounters() (*FeedCounters, error) {
|
func (c *Client) FetchCounters() (*FeedCounters, error) {
|
||||||
body, err := c.request.Get("/v1/feeds/counters")
|
body, err := c.request.Get("/v1/feeds/counters")
|
||||||
@ -613,28 +566,6 @@ func (c *Client) Icon(iconID int64) (*FeedIcon, error) {
|
|||||||
return feedIcon, nil
|
return feedIcon, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enclosure fetches a specific enclosure.
|
|
||||||
func (c *Client) Enclosure(enclosureID int64) (*Enclosure, error) {
|
|
||||||
body, err := c.request.Get(fmt.Sprintf("/v1/enclosures/%d", enclosureID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer body.Close()
|
|
||||||
|
|
||||||
var enclosure *Enclosure
|
|
||||||
if err := json.NewDecoder(body).Decode(&enclosure); err != nil {
|
|
||||||
return nil, fmt.Errorf("miniflux: response error(%v)", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return enclosure, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateEnclosure updates an enclosure.
|
|
||||||
func (c *Client) UpdateEnclosure(enclosureID int64, enclosureUpdate *EnclosureUpdateRequest) error {
|
|
||||||
_, err := c.request.Put(fmt.Sprintf("/v1/enclosures/%d", enclosureID), enclosureUpdate)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildFilterQueryString(path string, filter *Filter) string {
|
func buildFilterQueryString(path string, filter *Filter) string {
|
||||||
if filter != nil {
|
if filter != nil {
|
||||||
values := url.Values{}
|
values := url.Values{}
|
||||||
@ -707,10 +638,6 @@ func buildFilterQueryString(path string, filter *Filter) string {
|
|||||||
values.Set("feed_id", strconv.FormatInt(filter.FeedID, 10))
|
values.Set("feed_id", strconv.FormatInt(filter.FeedID, 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
if filter.GloballyVisible {
|
|
||||||
values.Set("globally_visible", "true")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, status := range filter.Statuses {
|
for _, status := range filter.Statuses {
|
||||||
values.Add("status", status)
|
values.Add("status", status)
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ This code snippet fetch the list of users:
|
|||||||
miniflux "miniflux.app/v2/client"
|
miniflux "miniflux.app/v2/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
client := miniflux.NewClient("https://api.example.org", "admin", "secret")
|
client := miniflux.New("https://api.example.org", "admin", "secret")
|
||||||
users, err := client.Users()
|
users, err := client.Users()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
@ -41,9 +41,6 @@ type User struct {
|
|||||||
DefaultHomePage string `json:"default_home_page"`
|
DefaultHomePage string `json:"default_home_page"`
|
||||||
CategoriesSortingOrder string `json:"categories_sorting_order"`
|
CategoriesSortingOrder string `json:"categories_sorting_order"`
|
||||||
MarkReadOnView bool `json:"mark_read_on_view"`
|
MarkReadOnView bool `json:"mark_read_on_view"`
|
||||||
MediaPlaybackRate float64 `json:"media_playback_rate"`
|
|
||||||
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
|
||||||
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u User) String() string {
|
func (u User) String() string {
|
||||||
@ -61,31 +58,28 @@ type UserCreationRequest struct {
|
|||||||
|
|
||||||
// UserModificationRequest represents the request to update a user.
|
// UserModificationRequest represents the request to update a user.
|
||||||
type UserModificationRequest struct {
|
type UserModificationRequest struct {
|
||||||
Username *string `json:"username"`
|
Username *string `json:"username"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
IsAdmin *bool `json:"is_admin"`
|
IsAdmin *bool `json:"is_admin"`
|
||||||
Theme *string `json:"theme"`
|
Theme *string `json:"theme"`
|
||||||
Language *string `json:"language"`
|
Language *string `json:"language"`
|
||||||
Timezone *string `json:"timezone"`
|
Timezone *string `json:"timezone"`
|
||||||
EntryDirection *string `json:"entry_sorting_direction"`
|
EntryDirection *string `json:"entry_sorting_direction"`
|
||||||
EntryOrder *string `json:"entry_sorting_order"`
|
EntryOrder *string `json:"entry_sorting_order"`
|
||||||
Stylesheet *string `json:"stylesheet"`
|
Stylesheet *string `json:"stylesheet"`
|
||||||
GoogleID *string `json:"google_id"`
|
GoogleID *string `json:"google_id"`
|
||||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||||
EntriesPerPage *int `json:"entries_per_page"`
|
EntriesPerPage *int `json:"entries_per_page"`
|
||||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||||
ShowReadingTime *bool `json:"show_reading_time"`
|
ShowReadingTime *bool `json:"show_reading_time"`
|
||||||
EntrySwipe *bool `json:"entry_swipe"`
|
EntrySwipe *bool `json:"entry_swipe"`
|
||||||
GestureNav *string `json:"gesture_nav"`
|
GestureNav *string `json:"gesture_nav"`
|
||||||
DisplayMode *string `json:"display_mode"`
|
DisplayMode *string `json:"display_mode"`
|
||||||
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
||||||
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
||||||
DefaultHomePage *string `json:"default_home_page"`
|
DefaultHomePage *string `json:"default_home_page"`
|
||||||
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
||||||
MarkReadOnView *bool `json:"mark_read_on_view"`
|
MarkReadOnView *bool `json:"mark_read_on_view"`
|
||||||
MediaPlaybackRate *float64 `json:"media_playback_rate"`
|
|
||||||
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
|
|
||||||
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Users represents a list of users.
|
// Users represents a list of users.
|
||||||
@ -242,17 +236,12 @@ type Entries []*Entry
|
|||||||
|
|
||||||
// Enclosure represents an attachment.
|
// Enclosure represents an attachment.
|
||||||
type Enclosure struct {
|
type Enclosure struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
EntryID int64 `json:"entry_id"`
|
EntryID int64 `json:"entry_id"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
MimeType string `json:"mime_type"`
|
MimeType string `json:"mime_type"`
|
||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
MediaProgression int64 `json:"media_progression"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EnclosureUpdateRequest struct {
|
|
||||||
MediaProgression int64 `json:"media_progression"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enclosures represents a list of attachments.
|
// Enclosures represents a list of attachments.
|
||||||
@ -283,7 +272,6 @@ type Filter struct {
|
|||||||
CategoryID int64
|
CategoryID int64
|
||||||
FeedID int64
|
FeedID int64
|
||||||
Statuses []string
|
Statuses []string
|
||||||
GloballyVisible bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EntryResultSet represents the response when fetching entries.
|
// EntryResultSet represents the response when fetching entries.
|
||||||
@ -302,7 +290,3 @@ type VersionResponse struct {
|
|||||||
Arch string `json:"arch"`
|
Arch string `json:"arch"`
|
||||||
OS string `json:"os"`
|
OS string `json:"os"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetOptionalField[T any](value T) *T {
|
|
||||||
return &value
|
|
||||||
}
|
|
||||||
|
@ -26,8 +26,6 @@ var (
|
|||||||
ErrForbidden = errors.New("miniflux: access forbidden")
|
ErrForbidden = errors.New("miniflux: access forbidden")
|
||||||
ErrServerError = errors.New("miniflux: internal server error")
|
ErrServerError = errors.New("miniflux: internal server error")
|
||||||
ErrNotFound = errors.New("miniflux: resource not found")
|
ErrNotFound = errors.New("miniflux: resource not found")
|
||||||
ErrBadRequest = errors.New("miniflux: bad request")
|
|
||||||
ErrEmptyEndpoint = errors.New("miniflux: empty endpoint provided")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type errorResponse struct {
|
type errorResponse struct {
|
||||||
@ -63,9 +61,6 @@ func (r *request) Delete(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *request) execute(method, path string, data interface{}) (io.ReadCloser, error) {
|
func (r *request) execute(method, path string, data interface{}) (io.ReadCloser, error) {
|
||||||
if r.endpoint == "" {
|
|
||||||
return nil, ErrEmptyEndpoint
|
|
||||||
}
|
|
||||||
if r.endpoint[len(r.endpoint)-1:] == "/" {
|
if r.endpoint[len(r.endpoint)-1:] == "/" {
|
||||||
r.endpoint = r.endpoint[:len(r.endpoint)-1]
|
r.endpoint = r.endpoint[:len(r.endpoint)-1]
|
||||||
}
|
}
|
||||||
@ -129,10 +124,10 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
|
|||||||
var resp errorResponse
|
var resp errorResponse
|
||||||
decoder := json.NewDecoder(response.Body)
|
decoder := json.NewDecoder(response.Body)
|
||||||
if err := decoder.Decode(&resp); err != nil {
|
if err := decoder.Decode(&resp); err != nil {
|
||||||
return nil, fmt.Errorf("%w (%v)", ErrBadRequest, err)
|
return nil, fmt.Errorf("miniflux: bad request error (%v)", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("%w (%s)", ErrBadRequest, resp.ErrorMessage)
|
return nil, fmt.Errorf("miniflux: bad request (%s)", resp.ErrorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.StatusCode > 400 {
|
if response.StatusCode > 400 {
|
||||||
|
File diff suppressed because it is too large
Load Diff
55
go.mod
55
go.mod
@ -1,49 +1,48 @@
|
|||||||
module miniflux.app/v2
|
module miniflux.app/v2
|
||||||
|
|
||||||
// +heroku goVersion go1.23
|
// +heroku goVersion go1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.10.0
|
github.com/PuerkitoBio/goquery v1.9.1
|
||||||
github.com/abadojack/whatlanggo v1.0.1
|
github.com/abadojack/whatlanggo v1.0.1
|
||||||
github.com/andybalholm/brotli v1.1.0
|
github.com/coreos/go-oidc/v3 v3.9.0
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0
|
github.com/go-webauthn/webauthn v0.10.1
|
||||||
github.com/go-webauthn/webauthn v0.11.2
|
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
github.com/prometheus/client_golang v1.20.4
|
github.com/prometheus/client_golang v1.19.0
|
||||||
github.com/tdewolff/minify/v2 v2.20.37
|
github.com/tdewolff/minify/v2 v2.20.18
|
||||||
github.com/yuin/goldmark v1.7.4
|
github.com/yuin/goldmark v1.7.0
|
||||||
golang.org/x/crypto v0.27.0
|
golang.org/x/crypto v0.21.0
|
||||||
golang.org/x/net v0.29.0
|
golang.org/x/net v0.22.0
|
||||||
golang.org/x/oauth2 v0.23.0
|
golang.org/x/oauth2 v0.18.0
|
||||||
golang.org/x/term v0.24.0
|
golang.org/x/term v0.18.0
|
||||||
golang.org/x/text v0.18.0
|
|
||||||
mvdan.cc/xurls/v2 v2.5.0
|
mvdan.cc/xurls/v2 v2.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-webauthn/x v0.1.14 // indirect
|
github.com/go-webauthn/x v0.1.8 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
|
||||||
github.com/google/go-tpm v0.9.1 // indirect
|
github.com/google/go-tpm v0.9.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/common v0.48.0 // indirect
|
||||||
github.com/prometheus/common v0.55.0 // indirect
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/tdewolff/parse/v2 v2.7.12 // indirect
|
||||||
github.com/tdewolff/parse/v2 v2.7.15 // indirect
|
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
golang.org/x/sys v0.25.0 // indirect
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
|
google.golang.org/protobuf v1.32.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.23
|
go 1.22
|
||||||
|
127
go.sum
127
go.sum
@ -1,75 +1,77 @@
|
|||||||
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
|
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
|
||||||
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
|
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
|
||||||
github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4=
|
github.com/abadojack/whatlanggo v1.0.1 h1:19N6YogDnf71CTHm3Mp2qhYfkRdyvbgwWdd2EPxJRG4=
|
||||||
github.com/abadojack/whatlanggo v1.0.1/go.mod h1:66WiQbSbJBIlOZMsvbKe5m6pzQovxCH9B/K8tQB2uoc=
|
github.com/abadojack/whatlanggo v1.0.1/go.mod h1:66WiQbSbJBIlOZMsvbKe5m6pzQovxCH9B/K8tQB2uoc=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
|
||||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||||
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
|
github.com/go-webauthn/webauthn v0.10.1 h1:+RFKj4yHPy282teiiy5sqTYPfRilzBpJyedrz9KsNFE=
|
||||||
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
|
github.com/go-webauthn/webauthn v0.10.1/go.mod h1:a7BwAtrSMkeuJXtIKz433Av99nAv01pdfzB0a9xkDnI=
|
||||||
github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0=
|
github.com/go-webauthn/x v0.1.8 h1:f1C6k1AyUlDvnIzWSW+G9rN9nbp1hhLXZagUtyxZ8nc=
|
||||||
github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc=
|
github.com/go-webauthn/x v0.1.8/go.mod h1:i8UNlGVt3oy6oAFcP4SZB1djZLx/4pbekCbWowjTaJg=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM=
|
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
|
||||||
github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
|
||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
|
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||||
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/tdewolff/minify/v2 v2.20.37 h1:Q97cx4STXCh1dlWDlNHZniE8BJ2EBL0+2b0n92BJQhw=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/tdewolff/minify/v2 v2.20.37/go.mod h1:L1VYef/jwKw6Wwyk5A+T0mBjjn3mMPgmjjA688RNsxU=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGpCTw=
|
github.com/tdewolff/minify/v2 v2.20.18 h1:y+s6OzlZwFqApgNXWNtaMuEMEPbHT72zrCyb9Az35Xo=
|
||||||
github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
github.com/tdewolff/minify/v2 v2.20.18/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM=
|
||||||
|
github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ=
|
||||||
|
github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
||||||
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||||
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
|
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
|
||||||
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
|
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
|
||||||
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||||
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@ -77,10 +79,11 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||||
|
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@ -91,28 +94,40 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||||
|
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||||
|
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||||
|
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
||||||
|
@ -72,8 +72,6 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
|
|||||||
sr.HandleFunc("/entries/{entryID}/fetch-content", handler.fetchContent).Methods(http.MethodGet)
|
sr.HandleFunc("/entries/{entryID}/fetch-content", handler.fetchContent).Methods(http.MethodGet)
|
||||||
sr.HandleFunc("/flush-history", handler.flushHistory).Methods(http.MethodPut, http.MethodDelete)
|
sr.HandleFunc("/flush-history", handler.flushHistory).Methods(http.MethodPut, http.MethodDelete)
|
||||||
sr.HandleFunc("/icons/{iconID}", handler.getIconByIconID).Methods(http.MethodGet)
|
sr.HandleFunc("/icons/{iconID}", handler.getIconByIconID).Methods(http.MethodGet)
|
||||||
sr.HandleFunc("/enclosures/{enclosureID}", handler.getEnclosureByID).Methods(http.MethodGet)
|
|
||||||
sr.HandleFunc("/enclosures/{enclosureID}", handler.updateEnclosureByID).Methods(http.MethodPut)
|
|
||||||
sr.HandleFunc("/version", handler.versionHandler).Methods(http.MethodGet)
|
sr.HandleFunc("/version", handler.versionHandler).Methods(http.MethodGet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,79 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package api // import "miniflux.app/v2/internal/api"
|
|
||||||
|
|
||||||
import (
|
|
||||||
json_parser "encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"miniflux.app/v2/internal/http/request"
|
|
||||||
"miniflux.app/v2/internal/http/response/json"
|
|
||||||
"miniflux.app/v2/internal/model"
|
|
||||||
"miniflux.app/v2/internal/validator"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *handler) getEnclosureByID(w http.ResponseWriter, r *http.Request) {
|
|
||||||
enclosureID := request.RouteInt64Param(r, "enclosureID")
|
|
||||||
|
|
||||||
enclosure, err := h.store.GetEnclosure(enclosureID)
|
|
||||||
if err != nil {
|
|
||||||
json.ServerError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if enclosure == nil {
|
|
||||||
json.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := request.UserID(r)
|
|
||||||
if enclosure.UserID != userID {
|
|
||||||
json.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
enclosure.ProxifyEnclosureURL(h.router)
|
|
||||||
|
|
||||||
json.OK(w, r, enclosure)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) updateEnclosureByID(w http.ResponseWriter, r *http.Request) {
|
|
||||||
enclosureID := request.RouteInt64Param(r, "enclosureID")
|
|
||||||
|
|
||||||
var enclosureUpdateRequest model.EnclosureUpdateRequest
|
|
||||||
if err := json_parser.NewDecoder(r.Body).Decode(&enclosureUpdateRequest); err != nil {
|
|
||||||
json.BadRequest(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validator.ValidateEnclosureUpdateRequest(&enclosureUpdateRequest); err != nil {
|
|
||||||
json.BadRequest(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
enclosure, err := h.store.GetEnclosure(enclosureID)
|
|
||||||
if err != nil {
|
|
||||||
json.ServerError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if enclosure == nil {
|
|
||||||
json.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := request.UserID(r)
|
|
||||||
if enclosure.UserID != userID {
|
|
||||||
json.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
enclosure.MediaProgression = enclosureUpdateRequest.MediaProgression
|
|
||||||
if err := h.store.UpdateEnclosure(enclosure); err != nil {
|
|
||||||
json.ServerError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NoContent(w, r)
|
|
||||||
}
|
|
@ -8,16 +8,19 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"miniflux.app/v2/internal/config"
|
||||||
"miniflux.app/v2/internal/http/request"
|
"miniflux.app/v2/internal/http/request"
|
||||||
"miniflux.app/v2/internal/http/response/json"
|
"miniflux.app/v2/internal/http/response/json"
|
||||||
"miniflux.app/v2/internal/integration"
|
"miniflux.app/v2/internal/integration"
|
||||||
"miniflux.app/v2/internal/mediaproxy"
|
|
||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
|
"miniflux.app/v2/internal/proxy"
|
||||||
"miniflux.app/v2/internal/reader/processor"
|
"miniflux.app/v2/internal/reader/processor"
|
||||||
"miniflux.app/v2/internal/reader/readingtime"
|
"miniflux.app/v2/internal/reader/readingtime"
|
||||||
"miniflux.app/v2/internal/storage"
|
"miniflux.app/v2/internal/storage"
|
||||||
|
"miniflux.app/v2/internal/urllib"
|
||||||
"miniflux.app/v2/internal/validator"
|
"miniflux.app/v2/internal/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,9 +36,19 @@ func (h *handler) getEntryFromBuilder(w http.ResponseWriter, r *http.Request, b
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content)
|
entry.Content = proxy.AbsoluteProxyRewriter(h.router, r.Host, entry.Content)
|
||||||
|
proxyOption := config.Opts.ProxyOption()
|
||||||
|
|
||||||
entry.Enclosures.ProxifyEnclosureURL(h.router)
|
for i := range entry.Enclosures {
|
||||||
|
if proxyOption == "all" || proxyOption != "none" && !urllib.IsHTTPS(entry.Enclosures[i].URL) {
|
||||||
|
for _, mediaType := range config.Opts.ProxyMediaTypes() {
|
||||||
|
if strings.HasPrefix(entry.Enclosures[i].MimeType, mediaType+"/") {
|
||||||
|
entry.Enclosures[i].URL = proxy.AbsoluteProxifyURL(h.router, r.Host, entry.Enclosures[i].URL)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
json.OK(w, r, entry)
|
json.OK(w, r, entry)
|
||||||
}
|
}
|
||||||
@ -136,15 +149,6 @@ func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int
|
|||||||
builder.WithLimit(limit)
|
builder.WithLimit(limit)
|
||||||
builder.WithTags(tags)
|
builder.WithTags(tags)
|
||||||
builder.WithEnclosures()
|
builder.WithEnclosures()
|
||||||
|
|
||||||
if request.HasQueryParam(r, "globally_visible") {
|
|
||||||
globallyVisible := request.QueryBoolParam(r, "globally_visible", true)
|
|
||||||
|
|
||||||
if globallyVisible {
|
|
||||||
builder.WithGloballyVisible()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configureFilters(builder, r)
|
configureFilters(builder, r)
|
||||||
|
|
||||||
entries, err := builder.GetEntries()
|
entries, err := builder.GetEntries()
|
||||||
@ -160,7 +164,7 @@ func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range entries {
|
for i := range entries {
|
||||||
entries[i].Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entries[i].Content)
|
entries[i].Content = proxy.AbsoluteProxyRewriter(h.router, r.Host, entries[i].Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
json.OK(w, r, &entriesResponse{Total: count, Entries: entries})
|
json.OK(w, r, &entriesResponse{Total: count, Entries: entries})
|
||||||
|
@ -115,7 +115,7 @@ func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if validationErr := validator.ValidateFeedModification(h.store, userID, originalFeed.ID, &feedModificationRequest); validationErr != nil {
|
if validationErr := validator.ValidateFeedModification(h.store, userID, &feedModificationRequest); validationErr != nil {
|
||||||
json.BadRequest(w, r, validationErr.Error())
|
json.BadRequest(w, r, validationErr.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,6 @@ import (
|
|||||||
json_parser "encoding/json"
|
json_parser "encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"miniflux.app/v2/internal/http/request"
|
"miniflux.app/v2/internal/http/request"
|
||||||
"miniflux.app/v2/internal/http/response/json"
|
"miniflux.app/v2/internal/http/response/json"
|
||||||
@ -84,18 +82,6 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanEnd := regexp.MustCompile(`(?m)\r\n\s*$`)
|
|
||||||
if userModificationRequest.BlockFilterEntryRules != nil {
|
|
||||||
*userModificationRequest.BlockFilterEntryRules = cleanEnd.ReplaceAllLiteralString(*userModificationRequest.BlockFilterEntryRules, "")
|
|
||||||
// Clean carriage returns for Windows environments
|
|
||||||
*userModificationRequest.BlockFilterEntryRules = strings.ReplaceAll(*userModificationRequest.BlockFilterEntryRules, "\r\n", "\n")
|
|
||||||
}
|
|
||||||
if userModificationRequest.KeepFilterEntryRules != nil {
|
|
||||||
*userModificationRequest.KeepFilterEntryRules = cleanEnd.ReplaceAllLiteralString(*userModificationRequest.KeepFilterEntryRules, "")
|
|
||||||
// Clean carriage returns for Windows environments
|
|
||||||
*userModificationRequest.KeepFilterEntryRules = strings.ReplaceAll(*userModificationRequest.KeepFilterEntryRules, "\r\n", "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if validationErr := validator.ValidateUserModification(h.store, originalUser.ID, &userModificationRequest); validationErr != nil {
|
if validationErr := validator.ValidateUserModification(h.store, originalUser.ID, &userModificationRequest); validationErr != nil {
|
||||||
json.BadRequest(w, r, validationErr.Error())
|
json.BadRequest(w, r, validationErr.Error())
|
||||||
return
|
return
|
||||||
|
@ -16,7 +16,7 @@ func askCredentials() (string, string) {
|
|||||||
fd := int(os.Stdin.Fd())
|
fd := int(os.Stdin.Fd())
|
||||||
|
|
||||||
if !term.IsTerminal(fd) {
|
if !term.IsTerminal(fd) {
|
||||||
printErrorAndExit(fmt.Errorf("this is not an interactive terminal, exiting"))
|
printErrorAndExit(fmt.Errorf("this is not a terminal, exiting"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print("Enter Username: ")
|
fmt.Print("Enter Username: ")
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
package cli // import "miniflux.app/v2/internal/cli"
|
package cli // import "miniflux.app/v2/internal/cli"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -24,7 +23,7 @@ const (
|
|||||||
flagVersionHelp = "Show application version"
|
flagVersionHelp = "Show application version"
|
||||||
flagMigrateHelp = "Run SQL migrations"
|
flagMigrateHelp = "Run SQL migrations"
|
||||||
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
|
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
|
||||||
flagCreateAdminHelp = "Create an admin user from an interactive terminal"
|
flagCreateAdminHelp = "Create admin user"
|
||||||
flagResetPasswordHelp = "Reset user password"
|
flagResetPasswordHelp = "Reset user password"
|
||||||
flagResetFeedErrorsHelp = "Clear all feed errors for all users"
|
flagResetFeedErrorsHelp = "Clear all feed errors for all users"
|
||||||
flagDebugModeHelp = "Show debug logs"
|
flagDebugModeHelp = "Show debug logs"
|
||||||
@ -89,23 +88,6 @@ func Parse() {
|
|||||||
printErrorAndExit(err)
|
printErrorAndExit(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if oauth2Provider := config.Opts.OAuth2Provider(); oauth2Provider != "" {
|
|
||||||
if oauth2Provider != "oidc" && oauth2Provider != "google" {
|
|
||||||
printErrorAndExit(fmt.Errorf(`unsupported OAuth2 provider: %q (Possible values are "google" or "oidc")`, oauth2Provider))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Opts.DisableLocalAuth() {
|
|
||||||
switch {
|
|
||||||
case config.Opts.OAuth2Provider() == "" && config.Opts.AuthProxyHeader() == "":
|
|
||||||
printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled but neither OAUTH2_PROVIDER nor AUTH_PROXY_HEADER is not set. Please enable at least one authentication source"))
|
|
||||||
case config.Opts.OAuth2Provider() != "" && !config.Opts.IsOAuth2UserCreationAllowed():
|
|
||||||
printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an OAUTH2_PROVIDER is configured, but OAUTH2_USER_CREATION is not enabled"))
|
|
||||||
case config.Opts.AuthProxyHeader() != "" && !config.Opts.IsAuthProxyUserCreationAllowed():
|
|
||||||
printErrorAndExit(errors.New("DISABLE_LOCAL_AUTH is enabled and an AUTH_PROXY_HEADER is configured, but AUTH_PROXY_USER_CREATION is not enabled"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if flagConfigDump {
|
if flagConfigDump {
|
||||||
fmt.Print(config.Opts)
|
fmt.Print(config.Opts)
|
||||||
return
|
return
|
||||||
@ -209,7 +191,7 @@ func Parse() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if flagCreateAdmin {
|
if flagCreateAdmin {
|
||||||
createAdminUserFromInteractiveTerminal(store)
|
createAdmin(store)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,8 +211,9 @@ func Parse() {
|
|||||||
printErrorAndExit(err)
|
printErrorAndExit(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create admin user and start the daemon.
|
||||||
if config.Opts.CreateAdmin() {
|
if config.Opts.CreateAdmin() {
|
||||||
createAdminUserFromEnvironmentVariables(store)
|
createAdmin(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
if flagRefreshFeeds {
|
if flagRefreshFeeds {
|
||||||
|
@ -12,22 +12,17 @@ import (
|
|||||||
"miniflux.app/v2/internal/validator"
|
"miniflux.app/v2/internal/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createAdminUserFromEnvironmentVariables(store *storage.Storage) {
|
func createAdmin(store *storage.Storage) {
|
||||||
createAdminUser(store, config.Opts.AdminUsername(), config.Opts.AdminPassword())
|
|
||||||
}
|
|
||||||
|
|
||||||
func createAdminUserFromInteractiveTerminal(store *storage.Storage) {
|
|
||||||
username, password := askCredentials()
|
|
||||||
createAdminUser(store, username, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createAdminUser(store *storage.Storage, username, password string) {
|
|
||||||
userCreationRequest := &model.UserCreationRequest{
|
userCreationRequest := &model.UserCreationRequest{
|
||||||
Username: username,
|
Username: config.Opts.AdminUsername(),
|
||||||
Password: password,
|
Password: config.Opts.AdminPassword(),
|
||||||
IsAdmin: true,
|
IsAdmin: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if userCreationRequest.Username == "" || userCreationRequest.Password == "" {
|
||||||
|
userCreationRequest.Username, userCreationRequest.Password = askCredentials()
|
||||||
|
}
|
||||||
|
|
||||||
if store.UserExists(userCreationRequest.Username) {
|
if store.UserExists(userCreationRequest.Username) {
|
||||||
slog.Info("Skipping admin user creation because it already exists",
|
slog.Info("Skipping admin user creation because it already exists",
|
||||||
slog.String("username", userCreationRequest.Username),
|
slog.String("username", userCreationRequest.Username),
|
||||||
@ -39,12 +34,7 @@ func createAdminUser(store *storage.Storage, username, password string) {
|
|||||||
printErrorAndExit(validationErr.Error())
|
printErrorAndExit(validationErr.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if user, err := store.CreateUser(userCreationRequest); err != nil {
|
if _, err := store.CreateUser(userCreationRequest); err != nil {
|
||||||
printErrorAndExit(err)
|
printErrorAndExit(err)
|
||||||
} else {
|
|
||||||
slog.Info("Created new admin user",
|
|
||||||
slog.String("username", user.Username),
|
|
||||||
slog.Int64("user_id", user.ID),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
package config // import "miniflux.app/v2/internal/config"
|
package config // import "miniflux.app/v2/internal/config"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -259,29 +258,6 @@ func TestCustomBaseURLWithTrailingSlash(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCustomBaseURLWithCustomPort(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("BASE_URL", "http://example.org:88/folder/")
|
|
||||||
|
|
||||||
parser := NewParser()
|
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.BaseURL() != "http://example.org:88/folder" {
|
|
||||||
t.Fatalf(`Unexpected base URL, got "%s"`, opts.BaseURL())
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.RootURL() != "http://example.org:88" {
|
|
||||||
t.Fatalf(`Unexpected root URL, got "%s"`, opts.RootURL())
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.BasePath() != "/folder" {
|
|
||||||
t.Fatalf(`Unexpected base path, got "%s"`, opts.BasePath())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBaseURLWithoutScheme(t *testing.T) {
|
func TestBaseURLWithoutScheme(t *testing.T) {
|
||||||
os.Clearenv()
|
os.Clearenv()
|
||||||
os.Setenv("BASE_URL", "example.org/folder/")
|
os.Setenv("BASE_URL", "example.org/folder/")
|
||||||
@ -1466,9 +1442,9 @@ func TestPocketConsumerKeyFromUserPrefs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMediaProxyMode(t *testing.T) {
|
func TestProxyOption(t *testing.T) {
|
||||||
os.Clearenv()
|
os.Clearenv()
|
||||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
os.Setenv("PROXY_OPTION", "all")
|
||||||
|
|
||||||
parser := NewParser()
|
parser := NewParser()
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
opts, err := parser.ParseEnvironmentVariables()
|
||||||
@ -1477,14 +1453,14 @@ func TestMediaProxyMode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expected := "all"
|
expected := "all"
|
||||||
result := opts.MediaProxyMode()
|
result := opts.ProxyOption()
|
||||||
|
|
||||||
if result != expected {
|
if result != expected {
|
||||||
t.Fatalf(`Unexpected MEDIA_PROXY_MODE value, got %q instead of %q`, result, expected)
|
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultMediaProxyModeValue(t *testing.T) {
|
func TestDefaultProxyOptionValue(t *testing.T) {
|
||||||
os.Clearenv()
|
os.Clearenv()
|
||||||
|
|
||||||
parser := NewParser()
|
parser := NewParser()
|
||||||
@ -1493,17 +1469,17 @@ func TestDefaultMediaProxyModeValue(t *testing.T) {
|
|||||||
t.Fatalf(`Parsing failure: %v`, err)
|
t.Fatalf(`Parsing failure: %v`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := defaultMediaProxyMode
|
expected := defaultProxyOption
|
||||||
result := opts.MediaProxyMode()
|
result := opts.ProxyOption()
|
||||||
|
|
||||||
if result != expected {
|
if result != expected {
|
||||||
t.Fatalf(`Unexpected MEDIA_PROXY_MODE value, got %q instead of %q`, result, expected)
|
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMediaProxyResourceTypes(t *testing.T) {
|
func TestProxyMediaTypes(t *testing.T) {
|
||||||
os.Clearenv()
|
os.Clearenv()
|
||||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image,audio")
|
os.Setenv("PROXY_MEDIA_TYPES", "image,audio")
|
||||||
|
|
||||||
parser := NewParser()
|
parser := NewParser()
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
opts, err := parser.ParseEnvironmentVariables()
|
||||||
@ -1513,25 +1489,25 @@ func TestMediaProxyResourceTypes(t *testing.T) {
|
|||||||
|
|
||||||
expected := []string{"audio", "image"}
|
expected := []string{"audio", "image"}
|
||||||
|
|
||||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
if len(expected) != len(opts.ProxyMediaTypes()) {
|
||||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
resultMap := make(map[string]bool)
|
resultMap := make(map[string]bool)
|
||||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
for _, mediaType := range opts.ProxyMediaTypes() {
|
||||||
resultMap[mediaType] = true
|
resultMap[mediaType] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, mediaType := range expected {
|
for _, mediaType := range expected {
|
||||||
if !resultMap[mediaType] {
|
if !resultMap[mediaType] {
|
||||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMediaProxyResourceTypesWithDuplicatedValues(t *testing.T) {
|
func TestProxyMediaTypesWithDuplicatedValues(t *testing.T) {
|
||||||
os.Clearenv()
|
os.Clearenv()
|
||||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image,audio, image")
|
os.Setenv("PROXY_MEDIA_TYPES", "image,audio, image")
|
||||||
|
|
||||||
parser := NewParser()
|
parser := NewParser()
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
opts, err := parser.ParseEnvironmentVariables()
|
||||||
@ -1540,119 +1516,23 @@ func TestMediaProxyResourceTypesWithDuplicatedValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expected := []string{"audio", "image"}
|
expected := []string{"audio", "image"}
|
||||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
if len(expected) != len(opts.ProxyMediaTypes()) {
|
||||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
resultMap := make(map[string]bool)
|
resultMap := make(map[string]bool)
|
||||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
for _, mediaType := range opts.ProxyMediaTypes() {
|
||||||
resultMap[mediaType] = true
|
resultMap[mediaType] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, mediaType := range expected {
|
for _, mediaType := range expected {
|
||||||
if !resultMap[mediaType] {
|
if !resultMap[mediaType] {
|
||||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultMediaProxyResourceTypes(t *testing.T) {
|
func TestProxyImagesOptionBackwardCompatibility(t *testing.T) {
|
||||||
os.Clearenv()
|
|
||||||
|
|
||||||
parser := NewParser()
|
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := []string{"image"}
|
|
||||||
|
|
||||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
|
||||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
resultMap := make(map[string]bool)
|
|
||||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
|
||||||
resultMap[mediaType] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mediaType := range expected {
|
|
||||||
if !resultMap[mediaType] {
|
|
||||||
t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMediaProxyHTTPClientTimeout(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("MEDIA_PROXY_HTTP_CLIENT_TIMEOUT", "24")
|
|
||||||
|
|
||||||
parser := NewParser()
|
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := 24
|
|
||||||
result := opts.MediaProxyHTTPClientTimeout()
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Fatalf(`Unexpected MEDIA_PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultMediaProxyHTTPClientTimeoutValue(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
|
|
||||||
parser := NewParser()
|
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := defaultMediaProxyHTTPClientTimeout
|
|
||||||
result := opts.MediaProxyHTTPClientTimeout()
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Fatalf(`Unexpected MEDIA_PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMediaProxyCustomURL(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("MEDIA_PROXY_CUSTOM_URL", "http://example.org/proxy")
|
|
||||||
|
|
||||||
parser := NewParser()
|
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
expected := "http://example.org/proxy"
|
|
||||||
result := opts.MediaCustomProxyURL()
|
|
||||||
if result != expected {
|
|
||||||
t.Fatalf(`Unexpected MEDIA_PROXY_CUSTOM_URL value, got %q instead of %q`, result, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMediaProxyPrivateKey(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "foobar")
|
|
||||||
|
|
||||||
parser := NewParser()
|
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := []byte("foobar")
|
|
||||||
result := opts.MediaProxyPrivateKey()
|
|
||||||
|
|
||||||
if !bytes.Equal(result, expected) {
|
|
||||||
t.Fatalf(`Unexpected MEDIA_PROXY_PRIVATE_KEY value, got %q instead of %q`, result, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProxyImagesOptionForBackwardCompatibility(t *testing.T) {
|
|
||||||
os.Clearenv()
|
os.Clearenv()
|
||||||
os.Setenv("PROXY_IMAGES", "all")
|
os.Setenv("PROXY_IMAGES", "all")
|
||||||
|
|
||||||
@ -1663,31 +1543,30 @@ func TestProxyImagesOptionForBackwardCompatibility(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expected := []string{"image"}
|
expected := []string{"image"}
|
||||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
if len(expected) != len(opts.ProxyMediaTypes()) {
|
||||||
t.Fatalf(`Unexpected PROXY_IMAGES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
resultMap := make(map[string]bool)
|
resultMap := make(map[string]bool)
|
||||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
for _, mediaType := range opts.ProxyMediaTypes() {
|
||||||
resultMap[mediaType] = true
|
resultMap[mediaType] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, mediaType := range expected {
|
for _, mediaType := range expected {
|
||||||
if !resultMap[mediaType] {
|
if !resultMap[mediaType] {
|
||||||
t.Fatalf(`Unexpected PROXY_IMAGES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedProxyOption := "all"
|
expectedProxyOption := "all"
|
||||||
result := opts.MediaProxyMode()
|
result := opts.ProxyOption()
|
||||||
if result != expectedProxyOption {
|
if result != expectedProxyOption {
|
||||||
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expectedProxyOption)
|
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expectedProxyOption)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProxyImageURLForBackwardCompatibility(t *testing.T) {
|
func TestDefaultProxyMediaTypes(t *testing.T) {
|
||||||
os.Clearenv()
|
os.Clearenv()
|
||||||
os.Setenv("PROXY_IMAGE_URL", "http://example.org/proxy")
|
|
||||||
|
|
||||||
parser := NewParser()
|
parser := NewParser()
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
opts, err := parser.ParseEnvironmentVariables()
|
||||||
@ -1695,73 +1574,25 @@ func TestProxyImageURLForBackwardCompatibility(t *testing.T) {
|
|||||||
t.Fatalf(`Parsing failure: %v`, err)
|
t.Fatalf(`Parsing failure: %v`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := "http://example.org/proxy"
|
expected := []string{"image"}
|
||||||
result := opts.MediaCustomProxyURL()
|
|
||||||
if result != expected {
|
|
||||||
t.Fatalf(`Unexpected PROXY_IMAGE_URL value, got %q instead of %q`, result, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProxyURLOptionForBackwardCompatibility(t *testing.T) {
|
if len(expected) != len(opts.ProxyMediaTypes()) {
|
||||||
os.Clearenv()
|
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||||
os.Setenv("PROXY_URL", "http://example.org/proxy")
|
|
||||||
|
|
||||||
parser := NewParser()
|
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := "http://example.org/proxy"
|
|
||||||
result := opts.MediaCustomProxyURL()
|
|
||||||
if result != expected {
|
|
||||||
t.Fatalf(`Unexpected PROXY_URL value, got %q instead of %q`, result, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProxyMediaTypesOptionForBackwardCompatibility(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("PROXY_MEDIA_TYPES", "image,audio")
|
|
||||||
|
|
||||||
parser := NewParser()
|
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
expected := []string{"audio", "image"}
|
|
||||||
if len(expected) != len(opts.MediaProxyResourceTypes()) {
|
|
||||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resultMap := make(map[string]bool)
|
resultMap := make(map[string]bool)
|
||||||
for _, mediaType := range opts.MediaProxyResourceTypes() {
|
for _, mediaType := range opts.ProxyMediaTypes() {
|
||||||
resultMap[mediaType] = true
|
resultMap[mediaType] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, mediaType := range expected {
|
for _, mediaType := range expected {
|
||||||
if !resultMap[mediaType] {
|
if !resultMap[mediaType] {
|
||||||
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected)
|
t.Fatalf(`Unexpected PROXY_MEDIA_TYPES value, got %v instead of %v`, opts.ProxyMediaTypes(), expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProxyOptionForBackwardCompatibility(t *testing.T) {
|
func TestProxyHTTPClientTimeout(t *testing.T) {
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("PROXY_OPTION", "all")
|
|
||||||
|
|
||||||
parser := NewParser()
|
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
expected := "all"
|
|
||||||
result := opts.MediaProxyMode()
|
|
||||||
if result != expected {
|
|
||||||
t.Fatalf(`Unexpected PROXY_OPTION value, got %q instead of %q`, result, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProxyHTTPClientTimeoutOptionForBackwardCompatibility(t *testing.T) {
|
|
||||||
os.Clearenv()
|
os.Clearenv()
|
||||||
os.Setenv("PROXY_HTTP_CLIENT_TIMEOUT", "24")
|
os.Setenv("PROXY_HTTP_CLIENT_TIMEOUT", "24")
|
||||||
|
|
||||||
@ -1770,26 +1601,29 @@ func TestProxyHTTPClientTimeoutOptionForBackwardCompatibility(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
t.Fatalf(`Parsing failure: %v`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := 24
|
expected := 24
|
||||||
result := opts.MediaProxyHTTPClientTimeout()
|
result := opts.ProxyHTTPClientTimeout()
|
||||||
|
|
||||||
if result != expected {
|
if result != expected {
|
||||||
t.Fatalf(`Unexpected PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
t.Fatalf(`Unexpected PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProxyPrivateKeyOptionForBackwardCompatibility(t *testing.T) {
|
func TestDefaultProxyHTTPClientTimeoutValue(t *testing.T) {
|
||||||
os.Clearenv()
|
os.Clearenv()
|
||||||
os.Setenv("PROXY_PRIVATE_KEY", "foobar")
|
|
||||||
|
|
||||||
parser := NewParser()
|
parser := NewParser()
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
opts, err := parser.ParseEnvironmentVariables()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
t.Fatalf(`Parsing failure: %v`, err)
|
||||||
}
|
}
|
||||||
expected := []byte("foobar")
|
|
||||||
result := opts.MediaProxyPrivateKey()
|
expected := defaultProxyHTTPClientTimeout
|
||||||
if !bytes.Equal(result, expected) {
|
result := opts.ProxyHTTPClientTimeout()
|
||||||
t.Fatalf(`Unexpected PROXY_PRIVATE_KEY value, got %q instead of %q`, result, expected)
|
|
||||||
|
if result != expected {
|
||||||
|
t.Fatalf(`Unexpected PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2044,42 +1878,6 @@ func TestAuthProxyUserCreationAdmin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetchBilibiliWatchTime(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("FETCH_BILIBILI_WATCH_TIME", "1")
|
|
||||||
|
|
||||||
parser := NewParser()
|
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := true
|
|
||||||
result := opts.FetchBilibiliWatchTime()
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Fatalf(`Unexpected FETCH_BILIBILI_WATCH_TIME value, got %v instead of %v`, result, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFetchNebulaWatchTime(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("FETCH_NEBULA_WATCH_TIME", "1")
|
|
||||||
|
|
||||||
parser := NewParser()
|
|
||||||
opts, err := parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := true
|
|
||||||
result := opts.FetchNebulaWatchTime()
|
|
||||||
|
|
||||||
if result != expected {
|
|
||||||
t.Fatalf(`Unexpected FETCH_NEBULA_WATCH_TIME value, got %v instead of %v`, result, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFetchOdyseeWatchTime(t *testing.T) {
|
func TestFetchOdyseeWatchTime(t *testing.T) {
|
||||||
os.Clearenv()
|
os.Clearenv()
|
||||||
os.Setenv("FETCH_ODYSEE_WATCH_TIME", "1")
|
os.Setenv("FETCH_ODYSEE_WATCH_TIME", "1")
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
package config // import "miniflux.app/v2/internal/config"
|
package config // import "miniflux.app/v2/internal/config"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miniflux.app/v2/internal/crypto"
|
|
||||||
"miniflux.app/v2/internal/version"
|
"miniflux.app/v2/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ const (
|
|||||||
defaultBaseURL = "http://localhost"
|
defaultBaseURL = "http://localhost"
|
||||||
defaultRootURL = "http://localhost"
|
defaultRootURL = "http://localhost"
|
||||||
defaultBasePath = ""
|
defaultBasePath = ""
|
||||||
defaultWorkerPoolSize = 16
|
defaultWorkerPoolSize = 5
|
||||||
defaultPollingFrequency = 60
|
defaultPollingFrequency = 60
|
||||||
defaultForceRefreshInterval = 30
|
defaultForceRefreshInterval = 30
|
||||||
defaultBatchSize = 100
|
defaultBatchSize = 100
|
||||||
@ -51,13 +51,10 @@ const (
|
|||||||
defaultCleanupArchiveUnreadDays = 180
|
defaultCleanupArchiveUnreadDays = 180
|
||||||
defaultCleanupArchiveBatchSize = 10000
|
defaultCleanupArchiveBatchSize = 10000
|
||||||
defaultCleanupRemoveSessionsDays = 30
|
defaultCleanupRemoveSessionsDays = 30
|
||||||
defaultMediaProxyHTTPClientTimeout = 120
|
defaultProxyHTTPClientTimeout = 120
|
||||||
defaultMediaProxyMode = "http-only"
|
defaultProxyOption = "http-only"
|
||||||
defaultMediaResourceTypes = "image"
|
defaultProxyMediaTypes = "image"
|
||||||
defaultMediaProxyURL = ""
|
defaultProxyUrl = ""
|
||||||
defaultFilterEntryMaxAgeDays = 0
|
|
||||||
defaultFetchBilibiliWatchTime = false
|
|
||||||
defaultFetchNebulaWatchTime = false
|
|
||||||
defaultFetchOdyseeWatchTime = false
|
defaultFetchOdyseeWatchTime = false
|
||||||
defaultFetchYouTubeWatchTime = false
|
defaultFetchYouTubeWatchTime = false
|
||||||
defaultYouTubeEmbedUrlOverride = "https://www.youtube-nocookie.com/embed/"
|
defaultYouTubeEmbedUrlOverride = "https://www.youtube-nocookie.com/embed/"
|
||||||
@ -69,9 +66,7 @@ const (
|
|||||||
defaultOAuth2ClientSecret = ""
|
defaultOAuth2ClientSecret = ""
|
||||||
defaultOAuth2RedirectURL = ""
|
defaultOAuth2RedirectURL = ""
|
||||||
defaultOAuth2OidcDiscoveryEndpoint = ""
|
defaultOAuth2OidcDiscoveryEndpoint = ""
|
||||||
defaultOauth2OidcProviderName = "OpenID Connect"
|
|
||||||
defaultOAuth2Provider = ""
|
defaultOAuth2Provider = ""
|
||||||
defaultDisableLocalAuth = false
|
|
||||||
defaultPocketConsumerKey = ""
|
defaultPocketConsumerKey = ""
|
||||||
defaultHTTPClientTimeout = 20
|
defaultHTTPClientTimeout = 20
|
||||||
defaultHTTPClientMaxBodySize = 15
|
defaultHTTPClientMaxBodySize = 15
|
||||||
@ -140,24 +135,19 @@ type Options struct {
|
|||||||
createAdmin bool
|
createAdmin bool
|
||||||
adminUsername string
|
adminUsername string
|
||||||
adminPassword string
|
adminPassword string
|
||||||
mediaProxyHTTPClientTimeout int
|
proxyHTTPClientTimeout int
|
||||||
mediaProxyMode string
|
proxyOption string
|
||||||
mediaProxyResourceTypes []string
|
proxyMediaTypes []string
|
||||||
mediaProxyCustomURL string
|
proxyUrl string
|
||||||
fetchBilibiliWatchTime bool
|
|
||||||
fetchNebulaWatchTime bool
|
|
||||||
fetchOdyseeWatchTime bool
|
fetchOdyseeWatchTime bool
|
||||||
fetchYouTubeWatchTime bool
|
fetchYouTubeWatchTime bool
|
||||||
filterEntryMaxAgeDays int
|
|
||||||
youTubeEmbedUrlOverride string
|
youTubeEmbedUrlOverride string
|
||||||
oauth2UserCreationAllowed bool
|
oauth2UserCreationAllowed bool
|
||||||
oauth2ClientID string
|
oauth2ClientID string
|
||||||
oauth2ClientSecret string
|
oauth2ClientSecret string
|
||||||
oauth2RedirectURL string
|
oauth2RedirectURL string
|
||||||
oidcDiscoveryEndpoint string
|
oidcDiscoveryEndpoint string
|
||||||
oidcProviderName string
|
|
||||||
oauth2Provider string
|
oauth2Provider string
|
||||||
disableLocalAuth bool
|
|
||||||
pocketConsumerKey string
|
pocketConsumerKey string
|
||||||
httpClientTimeout int
|
httpClientTimeout int
|
||||||
httpClientMaxBodySize int64
|
httpClientMaxBodySize int64
|
||||||
@ -175,12 +165,15 @@ type Options struct {
|
|||||||
metricsPassword string
|
metricsPassword string
|
||||||
watchdog bool
|
watchdog bool
|
||||||
invidiousInstance string
|
invidiousInstance string
|
||||||
mediaProxyPrivateKey []byte
|
proxyPrivateKey []byte
|
||||||
webAuthn bool
|
webAuthn bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOptions returns Options with default values.
|
// NewOptions returns Options with default values.
|
||||||
func NewOptions() *Options {
|
func NewOptions() *Options {
|
||||||
|
randomKey := make([]byte, 16)
|
||||||
|
rand.Read(randomKey)
|
||||||
|
|
||||||
return &Options{
|
return &Options{
|
||||||
HTTPS: defaultHTTPS,
|
HTTPS: defaultHTTPS,
|
||||||
logFile: defaultLogFile,
|
logFile: defaultLogFile,
|
||||||
@ -219,13 +212,10 @@ func NewOptions() *Options {
|
|||||||
pollingParsingErrorLimit: defaultPollingParsingErrorLimit,
|
pollingParsingErrorLimit: defaultPollingParsingErrorLimit,
|
||||||
workerPoolSize: defaultWorkerPoolSize,
|
workerPoolSize: defaultWorkerPoolSize,
|
||||||
createAdmin: defaultCreateAdmin,
|
createAdmin: defaultCreateAdmin,
|
||||||
mediaProxyHTTPClientTimeout: defaultMediaProxyHTTPClientTimeout,
|
proxyHTTPClientTimeout: defaultProxyHTTPClientTimeout,
|
||||||
mediaProxyMode: defaultMediaProxyMode,
|
proxyOption: defaultProxyOption,
|
||||||
mediaProxyResourceTypes: []string{defaultMediaResourceTypes},
|
proxyMediaTypes: []string{defaultProxyMediaTypes},
|
||||||
mediaProxyCustomURL: defaultMediaProxyURL,
|
proxyUrl: defaultProxyUrl,
|
||||||
filterEntryMaxAgeDays: defaultFilterEntryMaxAgeDays,
|
|
||||||
fetchBilibiliWatchTime: defaultFetchBilibiliWatchTime,
|
|
||||||
fetchNebulaWatchTime: defaultFetchNebulaWatchTime,
|
|
||||||
fetchOdyseeWatchTime: defaultFetchOdyseeWatchTime,
|
fetchOdyseeWatchTime: defaultFetchOdyseeWatchTime,
|
||||||
fetchYouTubeWatchTime: defaultFetchYouTubeWatchTime,
|
fetchYouTubeWatchTime: defaultFetchYouTubeWatchTime,
|
||||||
youTubeEmbedUrlOverride: defaultYouTubeEmbedUrlOverride,
|
youTubeEmbedUrlOverride: defaultYouTubeEmbedUrlOverride,
|
||||||
@ -234,9 +224,7 @@ func NewOptions() *Options {
|
|||||||
oauth2ClientSecret: defaultOAuth2ClientSecret,
|
oauth2ClientSecret: defaultOAuth2ClientSecret,
|
||||||
oauth2RedirectURL: defaultOAuth2RedirectURL,
|
oauth2RedirectURL: defaultOAuth2RedirectURL,
|
||||||
oidcDiscoveryEndpoint: defaultOAuth2OidcDiscoveryEndpoint,
|
oidcDiscoveryEndpoint: defaultOAuth2OidcDiscoveryEndpoint,
|
||||||
oidcProviderName: defaultOauth2OidcProviderName,
|
|
||||||
oauth2Provider: defaultOAuth2Provider,
|
oauth2Provider: defaultOAuth2Provider,
|
||||||
disableLocalAuth: defaultDisableLocalAuth,
|
|
||||||
pocketConsumerKey: defaultPocketConsumerKey,
|
pocketConsumerKey: defaultPocketConsumerKey,
|
||||||
httpClientTimeout: defaultHTTPClientTimeout,
|
httpClientTimeout: defaultHTTPClientTimeout,
|
||||||
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
|
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
|
||||||
@ -254,7 +242,7 @@ func NewOptions() *Options {
|
|||||||
metricsPassword: defaultMetricsPassword,
|
metricsPassword: defaultMetricsPassword,
|
||||||
watchdog: defaultWatchdog,
|
watchdog: defaultWatchdog,
|
||||||
invidiousInstance: defaultInvidiousInstance,
|
invidiousInstance: defaultInvidiousInstance,
|
||||||
mediaProxyPrivateKey: crypto.GenerateRandomBytes(16),
|
proxyPrivateKey: randomKey,
|
||||||
webAuthn: defaultWebAuthn,
|
webAuthn: defaultWebAuthn,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -457,21 +445,11 @@ func (o *Options) OIDCDiscoveryEndpoint() string {
|
|||||||
return o.oidcDiscoveryEndpoint
|
return o.oidcDiscoveryEndpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
// OIDCProviderName returns the OAuth2 OIDC provider's display name
|
|
||||||
func (o *Options) OIDCProviderName() string {
|
|
||||||
return o.oidcProviderName
|
|
||||||
}
|
|
||||||
|
|
||||||
// OAuth2Provider returns the name of the OAuth2 provider configured.
|
// OAuth2Provider returns the name of the OAuth2 provider configured.
|
||||||
func (o *Options) OAuth2Provider() string {
|
func (o *Options) OAuth2Provider() string {
|
||||||
return o.oauth2Provider
|
return o.oauth2Provider
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableLocalAUth returns true if the local user database should not be used to authenticate users
|
|
||||||
func (o *Options) DisableLocalAuth() bool {
|
|
||||||
return o.disableLocalAuth
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasHSTS returns true if HTTP Strict Transport Security is enabled.
|
// HasHSTS returns true if HTTP Strict Transport Security is enabled.
|
||||||
func (o *Options) HasHSTS() bool {
|
func (o *Options) HasHSTS() bool {
|
||||||
return o.hsts
|
return o.hsts
|
||||||
@ -508,47 +486,30 @@ func (o *Options) YouTubeEmbedUrlOverride() string {
|
|||||||
return o.youTubeEmbedUrlOverride
|
return o.youTubeEmbedUrlOverride
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchNebulaWatchTime returns true if the Nebula video duration
|
|
||||||
// should be fetched and used as a reading time.
|
|
||||||
func (o *Options) FetchNebulaWatchTime() bool {
|
|
||||||
return o.fetchNebulaWatchTime
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchOdyseeWatchTime returns true if the Odysee video duration
|
// FetchOdyseeWatchTime returns true if the Odysee video duration
|
||||||
// should be fetched and used as a reading time.
|
// should be fetched and used as a reading time.
|
||||||
func (o *Options) FetchOdyseeWatchTime() bool {
|
func (o *Options) FetchOdyseeWatchTime() bool {
|
||||||
return o.fetchOdyseeWatchTime
|
return o.fetchOdyseeWatchTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchBilibiliWatchTime returns true if the Bilibili video duration
|
// ProxyOption returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
|
||||||
// should be fetched and used as a reading time.
|
func (o *Options) ProxyOption() string {
|
||||||
func (o *Options) FetchBilibiliWatchTime() bool {
|
return o.proxyOption
|
||||||
return o.fetchBilibiliWatchTime
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MediaProxyMode returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
|
// ProxyMediaTypes returns a slice of media types to proxy.
|
||||||
func (o *Options) MediaProxyMode() string {
|
func (o *Options) ProxyMediaTypes() []string {
|
||||||
return o.mediaProxyMode
|
return o.proxyMediaTypes
|
||||||
}
|
}
|
||||||
|
|
||||||
// MediaProxyResourceTypes returns a slice of resource types to proxy.
|
// ProxyUrl returns a string of a URL to use to proxy image requests
|
||||||
func (o *Options) MediaProxyResourceTypes() []string {
|
func (o *Options) ProxyUrl() string {
|
||||||
return o.mediaProxyResourceTypes
|
return o.proxyUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
// MediaCustomProxyURL returns the custom proxy URL for medias.
|
// ProxyHTTPClientTimeout returns the time limit in seconds before the proxy HTTP client cancel the request.
|
||||||
func (o *Options) MediaCustomProxyURL() string {
|
func (o *Options) ProxyHTTPClientTimeout() int {
|
||||||
return o.mediaProxyCustomURL
|
return o.proxyHTTPClientTimeout
|
||||||
}
|
|
||||||
|
|
||||||
// MediaProxyHTTPClientTimeout returns the time limit in seconds before the proxy HTTP client cancel the request.
|
|
||||||
func (o *Options) MediaProxyHTTPClientTimeout() int {
|
|
||||||
return o.mediaProxyHTTPClientTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
// MediaProxyPrivateKey returns the private key used by the media proxy.
|
|
||||||
func (o *Options) MediaProxyPrivateKey() []byte {
|
|
||||||
return o.mediaProxyPrivateKey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasHTTPService returns true if the HTTP service is enabled.
|
// HasHTTPService returns true if the HTTP service is enabled.
|
||||||
@ -644,16 +605,16 @@ func (o *Options) InvidiousInstance() string {
|
|||||||
return o.invidiousInstance
|
return o.invidiousInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProxyPrivateKey returns the private key used by the media proxy
|
||||||
|
func (o *Options) ProxyPrivateKey() []byte {
|
||||||
|
return o.proxyPrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
// WebAuthn returns true if WebAuthn logins are supported
|
// WebAuthn returns true if WebAuthn logins are supported
|
||||||
func (o *Options) WebAuthn() bool {
|
func (o *Options) WebAuthn() bool {
|
||||||
return o.webAuthn
|
return o.webAuthn
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterEntryMaxAgeDays returns the number of days after which entries should be retained.
|
|
||||||
func (o *Options) FilterEntryMaxAgeDays() int {
|
|
||||||
return o.filterEntryMaxAgeDays
|
|
||||||
}
|
|
||||||
|
|
||||||
// SortedOptions returns options as a list of key value pairs, sorted by keys.
|
// SortedOptions returns options as a list of key value pairs, sorted by keys.
|
||||||
func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
||||||
var keyValues = map[string]interface{}{
|
var keyValues = map[string]interface{}{
|
||||||
@ -679,11 +640,8 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
|||||||
"DISABLE_HSTS": !o.hsts,
|
"DISABLE_HSTS": !o.hsts,
|
||||||
"DISABLE_HTTP_SERVICE": !o.httpService,
|
"DISABLE_HTTP_SERVICE": !o.httpService,
|
||||||
"DISABLE_SCHEDULER_SERVICE": !o.schedulerService,
|
"DISABLE_SCHEDULER_SERVICE": !o.schedulerService,
|
||||||
"FILTER_ENTRY_MAX_AGE_DAYS": o.filterEntryMaxAgeDays,
|
|
||||||
"FETCH_YOUTUBE_WATCH_TIME": o.fetchYouTubeWatchTime,
|
"FETCH_YOUTUBE_WATCH_TIME": o.fetchYouTubeWatchTime,
|
||||||
"FETCH_NEBULA_WATCH_TIME": o.fetchNebulaWatchTime,
|
|
||||||
"FETCH_ODYSEE_WATCH_TIME": o.fetchOdyseeWatchTime,
|
"FETCH_ODYSEE_WATCH_TIME": o.fetchOdyseeWatchTime,
|
||||||
"FETCH_BILIBILI_WATCH_TIME": o.fetchBilibiliWatchTime,
|
|
||||||
"HTTPS": o.HTTPS,
|
"HTTPS": o.HTTPS,
|
||||||
"HTTP_CLIENT_MAX_BODY_SIZE": o.httpClientMaxBodySize,
|
"HTTP_CLIENT_MAX_BODY_SIZE": o.httpClientMaxBodySize,
|
||||||
"HTTP_CLIENT_PROXY": o.httpClientProxy,
|
"HTTP_CLIENT_PROXY": o.httpClientProxy,
|
||||||
@ -708,21 +666,19 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
|
|||||||
"OAUTH2_CLIENT_ID": o.oauth2ClientID,
|
"OAUTH2_CLIENT_ID": o.oauth2ClientID,
|
||||||
"OAUTH2_CLIENT_SECRET": redactSecretValue(o.oauth2ClientSecret, redactSecret),
|
"OAUTH2_CLIENT_SECRET": redactSecretValue(o.oauth2ClientSecret, redactSecret),
|
||||||
"OAUTH2_OIDC_DISCOVERY_ENDPOINT": o.oidcDiscoveryEndpoint,
|
"OAUTH2_OIDC_DISCOVERY_ENDPOINT": o.oidcDiscoveryEndpoint,
|
||||||
"OAUTH2_OIDC_PROVIDER_NAME": o.oidcProviderName,
|
|
||||||
"OAUTH2_PROVIDER": o.oauth2Provider,
|
"OAUTH2_PROVIDER": o.oauth2Provider,
|
||||||
"OAUTH2_REDIRECT_URL": o.oauth2RedirectURL,
|
"OAUTH2_REDIRECT_URL": o.oauth2RedirectURL,
|
||||||
"OAUTH2_USER_CREATION": o.oauth2UserCreationAllowed,
|
"OAUTH2_USER_CREATION": o.oauth2UserCreationAllowed,
|
||||||
"DISABLE_LOCAL_AUTH": o.disableLocalAuth,
|
|
||||||
"POCKET_CONSUMER_KEY": redactSecretValue(o.pocketConsumerKey, redactSecret),
|
"POCKET_CONSUMER_KEY": redactSecretValue(o.pocketConsumerKey, redactSecret),
|
||||||
"POLLING_FREQUENCY": o.pollingFrequency,
|
"POLLING_FREQUENCY": o.pollingFrequency,
|
||||||
"FORCE_REFRESH_INTERVAL": o.forceRefreshInterval,
|
"FORCE_REFRESH_INTERVAL": o.forceRefreshInterval,
|
||||||
"POLLING_PARSING_ERROR_LIMIT": o.pollingParsingErrorLimit,
|
"POLLING_PARSING_ERROR_LIMIT": o.pollingParsingErrorLimit,
|
||||||
"POLLING_SCHEDULER": o.pollingScheduler,
|
"POLLING_SCHEDULER": o.pollingScheduler,
|
||||||
"MEDIA_PROXY_HTTP_CLIENT_TIMEOUT": o.mediaProxyHTTPClientTimeout,
|
"PROXY_HTTP_CLIENT_TIMEOUT": o.proxyHTTPClientTimeout,
|
||||||
"MEDIA_PROXY_RESOURCE_TYPES": o.mediaProxyResourceTypes,
|
"PROXY_MEDIA_TYPES": o.proxyMediaTypes,
|
||||||
"MEDIA_PROXY_MODE": o.mediaProxyMode,
|
"PROXY_OPTION": o.proxyOption,
|
||||||
"MEDIA_PROXY_PRIVATE_KEY": redactSecretValue(string(o.mediaProxyPrivateKey), redactSecret),
|
"PROXY_PRIVATE_KEY": redactSecretValue(string(o.proxyPrivateKey), redactSecret),
|
||||||
"MEDIA_PROXY_CUSTOM_URL": o.mediaProxyCustomURL,
|
"PROXY_URL": o.proxyUrl,
|
||||||
"ROOT_URL": o.rootURL,
|
"ROOT_URL": o.rootURL,
|
||||||
"RUN_MIGRATIONS": o.runMigrations,
|
"RUN_MIGRATIONS": o.runMigrations,
|
||||||
"SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": o.schedulerEntryFrequencyMaxInterval,
|
"SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": o.schedulerEntryFrequencyMaxInterval,
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -88,7 +87,6 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
|||||||
p.opts.logFormat = parsedValue
|
p.opts.logFormat = parsedValue
|
||||||
}
|
}
|
||||||
case "DEBUG":
|
case "DEBUG":
|
||||||
slog.Warn("The DEBUG environment variable is deprecated, use LOG_LEVEL instead")
|
|
||||||
parsedValue := parseBool(value, defaultDebug)
|
parsedValue := parseBool(value, defaultDebug)
|
||||||
if parsedValue {
|
if parsedValue {
|
||||||
p.opts.logLevel = "debug"
|
p.opts.logLevel = "debug"
|
||||||
@ -114,8 +112,6 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
|||||||
p.opts.databaseMinConns = parseInt(value, defaultDatabaseMinConns)
|
p.opts.databaseMinConns = parseInt(value, defaultDatabaseMinConns)
|
||||||
case "DATABASE_CONNECTION_LIFETIME":
|
case "DATABASE_CONNECTION_LIFETIME":
|
||||||
p.opts.databaseConnectionLifetime = parseInt(value, defaultDatabaseConnectionLifetime)
|
p.opts.databaseConnectionLifetime = parseInt(value, defaultDatabaseConnectionLifetime)
|
||||||
case "FILTER_ENTRY_MAX_AGE_DAYS":
|
|
||||||
p.opts.filterEntryMaxAgeDays = parseInt(value, defaultFilterEntryMaxAgeDays)
|
|
||||||
case "RUN_MIGRATIONS":
|
case "RUN_MIGRATIONS":
|
||||||
p.opts.runMigrations = parseBool(value, defaultRunMigrations)
|
p.opts.runMigrations = parseBool(value, defaultRunMigrations)
|
||||||
case "DISABLE_HSTS":
|
case "DISABLE_HSTS":
|
||||||
@ -162,41 +158,20 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
|||||||
p.opts.schedulerRoundRobinMinInterval = parseInt(value, defaultSchedulerRoundRobinMinInterval)
|
p.opts.schedulerRoundRobinMinInterval = parseInt(value, defaultSchedulerRoundRobinMinInterval)
|
||||||
case "POLLING_PARSING_ERROR_LIMIT":
|
case "POLLING_PARSING_ERROR_LIMIT":
|
||||||
p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
|
p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
|
||||||
|
// kept for compatibility purpose
|
||||||
case "PROXY_IMAGES":
|
case "PROXY_IMAGES":
|
||||||
slog.Warn("The PROXY_IMAGES environment variable is deprecated, use MEDIA_PROXY_MODE instead")
|
p.opts.proxyOption = parseString(value, defaultProxyOption)
|
||||||
p.opts.mediaProxyMode = parseString(value, defaultMediaProxyMode)
|
|
||||||
case "PROXY_HTTP_CLIENT_TIMEOUT":
|
case "PROXY_HTTP_CLIENT_TIMEOUT":
|
||||||
slog.Warn("The PROXY_HTTP_CLIENT_TIMEOUT environment variable is deprecated, use MEDIA_PROXY_HTTP_CLIENT_TIMEOUT instead")
|
p.opts.proxyHTTPClientTimeout = parseInt(value, defaultProxyHTTPClientTimeout)
|
||||||
p.opts.mediaProxyHTTPClientTimeout = parseInt(value, defaultMediaProxyHTTPClientTimeout)
|
|
||||||
case "MEDIA_PROXY_HTTP_CLIENT_TIMEOUT":
|
|
||||||
p.opts.mediaProxyHTTPClientTimeout = parseInt(value, defaultMediaProxyHTTPClientTimeout)
|
|
||||||
case "PROXY_OPTION":
|
case "PROXY_OPTION":
|
||||||
slog.Warn("The PROXY_OPTION environment variable is deprecated, use MEDIA_PROXY_MODE instead")
|
p.opts.proxyOption = parseString(value, defaultProxyOption)
|
||||||
p.opts.mediaProxyMode = parseString(value, defaultMediaProxyMode)
|
|
||||||
case "MEDIA_PROXY_MODE":
|
|
||||||
p.opts.mediaProxyMode = parseString(value, defaultMediaProxyMode)
|
|
||||||
case "PROXY_MEDIA_TYPES":
|
case "PROXY_MEDIA_TYPES":
|
||||||
slog.Warn("The PROXY_MEDIA_TYPES environment variable is deprecated, use MEDIA_PROXY_RESOURCE_TYPES instead")
|
p.opts.proxyMediaTypes = parseStringList(value, []string{defaultProxyMediaTypes})
|
||||||
p.opts.mediaProxyResourceTypes = parseStringList(value, []string{defaultMediaResourceTypes})
|
// kept for compatibility purpose
|
||||||
case "MEDIA_PROXY_RESOURCE_TYPES":
|
|
||||||
p.opts.mediaProxyResourceTypes = parseStringList(value, []string{defaultMediaResourceTypes})
|
|
||||||
case "PROXY_IMAGE_URL":
|
case "PROXY_IMAGE_URL":
|
||||||
slog.Warn("The PROXY_IMAGE_URL environment variable is deprecated, use MEDIA_PROXY_CUSTOM_URL instead")
|
p.opts.proxyUrl = parseString(value, defaultProxyUrl)
|
||||||
p.opts.mediaProxyCustomURL = parseString(value, defaultMediaProxyURL)
|
|
||||||
case "PROXY_URL":
|
case "PROXY_URL":
|
||||||
slog.Warn("The PROXY_URL environment variable is deprecated, use MEDIA_PROXY_CUSTOM_URL instead")
|
p.opts.proxyUrl = parseString(value, defaultProxyUrl)
|
||||||
p.opts.mediaProxyCustomURL = parseString(value, defaultMediaProxyURL)
|
|
||||||
case "PROXY_PRIVATE_KEY":
|
|
||||||
slog.Warn("The PROXY_PRIVATE_KEY environment variable is deprecated, use MEDIA_PROXY_PRIVATE_KEY instead")
|
|
||||||
randomKey := make([]byte, 16)
|
|
||||||
rand.Read(randomKey)
|
|
||||||
p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey)
|
|
||||||
case "MEDIA_PROXY_PRIVATE_KEY":
|
|
||||||
randomKey := make([]byte, 16)
|
|
||||||
rand.Read(randomKey)
|
|
||||||
p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey)
|
|
||||||
case "MEDIA_PROXY_CUSTOM_URL":
|
|
||||||
p.opts.mediaProxyCustomURL = parseString(value, defaultMediaProxyURL)
|
|
||||||
case "CREATE_ADMIN":
|
case "CREATE_ADMIN":
|
||||||
p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
|
p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
|
||||||
case "ADMIN_USERNAME":
|
case "ADMIN_USERNAME":
|
||||||
@ -225,12 +200,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
|||||||
p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL)
|
p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL)
|
||||||
case "OAUTH2_OIDC_DISCOVERY_ENDPOINT":
|
case "OAUTH2_OIDC_DISCOVERY_ENDPOINT":
|
||||||
p.opts.oidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint)
|
p.opts.oidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint)
|
||||||
case "OAUTH2_OIDC_PROVIDER_NAME":
|
|
||||||
p.opts.oidcProviderName = parseString(value, defaultOauth2OidcProviderName)
|
|
||||||
case "OAUTH2_PROVIDER":
|
case "OAUTH2_PROVIDER":
|
||||||
p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider)
|
p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider)
|
||||||
case "DISABLE_LOCAL_AUTH":
|
|
||||||
p.opts.disableLocalAuth = parseBool(value, defaultDisableLocalAuth)
|
|
||||||
case "HTTP_CLIENT_TIMEOUT":
|
case "HTTP_CLIENT_TIMEOUT":
|
||||||
p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout)
|
p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout)
|
||||||
case "HTTP_CLIENT_MAX_BODY_SIZE":
|
case "HTTP_CLIENT_MAX_BODY_SIZE":
|
||||||
@ -263,10 +234,6 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
|||||||
p.opts.metricsPassword = parseString(value, defaultMetricsPassword)
|
p.opts.metricsPassword = parseString(value, defaultMetricsPassword)
|
||||||
case "METRICS_PASSWORD_FILE":
|
case "METRICS_PASSWORD_FILE":
|
||||||
p.opts.metricsPassword = readSecretFile(value, defaultMetricsPassword)
|
p.opts.metricsPassword = readSecretFile(value, defaultMetricsPassword)
|
||||||
case "FETCH_BILIBILI_WATCH_TIME":
|
|
||||||
p.opts.fetchBilibiliWatchTime = parseBool(value, defaultFetchBilibiliWatchTime)
|
|
||||||
case "FETCH_NEBULA_WATCH_TIME":
|
|
||||||
p.opts.fetchNebulaWatchTime = parseBool(value, defaultFetchNebulaWatchTime)
|
|
||||||
case "FETCH_ODYSEE_WATCH_TIME":
|
case "FETCH_ODYSEE_WATCH_TIME":
|
||||||
p.opts.fetchOdyseeWatchTime = parseBool(value, defaultFetchOdyseeWatchTime)
|
p.opts.fetchOdyseeWatchTime = parseBool(value, defaultFetchOdyseeWatchTime)
|
||||||
case "FETCH_YOUTUBE_WATCH_TIME":
|
case "FETCH_YOUTUBE_WATCH_TIME":
|
||||||
@ -277,6 +244,10 @@ func (p *Parser) parseLines(lines []string) (err error) {
|
|||||||
p.opts.watchdog = parseBool(value, defaultWatchdog)
|
p.opts.watchdog = parseBool(value, defaultWatchdog)
|
||||||
case "INVIDIOUS_INSTANCE":
|
case "INVIDIOUS_INSTANCE":
|
||||||
p.opts.invidiousInstance = parseString(value, defaultInvidiousInstance)
|
p.opts.invidiousInstance = parseString(value, defaultInvidiousInstance)
|
||||||
|
case "PROXY_PRIVATE_KEY":
|
||||||
|
randomKey := make([]byte, 16)
|
||||||
|
rand.Read(randomKey)
|
||||||
|
p.opts.proxyPrivateKey = parseBytes(value, randomKey)
|
||||||
case "WEBAUTHN":
|
case "WEBAUTHN":
|
||||||
p.opts.webAuthn = parseBool(value, defaultWebAuthn)
|
p.opts.webAuthn = parseBool(value, defaultWebAuthn)
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,8 @@ import (
|
|||||||
|
|
||||||
// HashFromBytes returns a SHA-256 checksum of the input.
|
// HashFromBytes returns a SHA-256 checksum of the input.
|
||||||
func HashFromBytes(value []byte) string {
|
func HashFromBytes(value []byte) string {
|
||||||
return fmt.Sprintf("%x", sha256.Sum256(value))
|
sum := sha256.Sum256(value)
|
||||||
|
return fmt.Sprintf("%x", sum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash returns a SHA-256 checksum of a string.
|
// Hash returns a SHA-256 checksum of a string.
|
||||||
|
@ -32,7 +32,7 @@ func Migrate(db *sql.DB) error {
|
|||||||
var currentVersion int
|
var currentVersion int
|
||||||
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
db.QueryRow(`SELECT version FROM schema_version`).Scan(¤tVersion)
|
||||||
|
|
||||||
slog.Info("Running database migrations",
|
slog.Debug("Running database migrations",
|
||||||
slog.Int("current_version", currentVersion),
|
slog.Int("current_version", currentVersion),
|
||||||
slog.Int("latest_version", schemaVersion),
|
slog.Int("latest_version", schemaVersion),
|
||||||
)
|
)
|
||||||
|
@ -871,75 +871,4 @@ var migrations = []func(tx *sql.Tx) error{
|
|||||||
_, err = tx.Exec(sql)
|
_, err = tx.Exec(sql)
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
func(tx *sql.Tx) (err error) {
|
|
||||||
sql := `ALTER TABLE users ADD COLUMN media_playback_rate numeric default 1;`
|
|
||||||
_, err = tx.Exec(sql)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
func(tx *sql.Tx) (err error) {
|
|
||||||
// the WHERE part speed-up the request a lot
|
|
||||||
sql := `UPDATE entries SET tags = array_remove(tags, '') WHERE '' = ANY(tags);`
|
|
||||||
_, err = tx.Exec(sql)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
func(tx *sql.Tx) (err error) {
|
|
||||||
// Entry URLs can exceeds btree maximum size
|
|
||||||
// Checking entry existence is now using entries_feed_id_status_hash_idx index
|
|
||||||
_, err = tx.Exec(`DROP INDEX entries_feed_url_idx`)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
func(tx *sql.Tx) (err error) {
|
|
||||||
sql := `
|
|
||||||
ALTER TABLE integrations ADD COLUMN raindrop_enabled bool default 'f';
|
|
||||||
ALTER TABLE integrations ADD COLUMN raindrop_token text default '';
|
|
||||||
ALTER TABLE integrations ADD COLUMN raindrop_collection_id text default '';
|
|
||||||
ALTER TABLE integrations ADD COLUMN raindrop_tags text default '';
|
|
||||||
`
|
|
||||||
_, err = tx.Exec(sql)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
func(tx *sql.Tx) (err error) {
|
|
||||||
sql := `ALTER TABLE feeds ADD COLUMN description text default ''`
|
|
||||||
_, err = tx.Exec(sql)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
func(tx *sql.Tx) (err error) {
|
|
||||||
sql := `
|
|
||||||
ALTER TABLE users
|
|
||||||
ADD COLUMN block_filter_entry_rules text not null default '',
|
|
||||||
ADD COLUMN keep_filter_entry_rules text not null default ''
|
|
||||||
`
|
|
||||||
_, err = tx.Exec(sql)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
func(tx *sql.Tx) (err error) {
|
|
||||||
sql := `
|
|
||||||
ALTER TABLE integrations ADD COLUMN betula_url text default '';
|
|
||||||
ALTER TABLE integrations ADD COLUMN betula_token text default '';
|
|
||||||
ALTER TABLE integrations ADD COLUMN betula_enabled bool default 'f';
|
|
||||||
`
|
|
||||||
_, err = tx.Exec(sql)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
func(tx *sql.Tx) (err error) {
|
|
||||||
sql := `
|
|
||||||
ALTER TABLE integrations ADD COLUMN ntfy_enabled bool default 'f';
|
|
||||||
ALTER TABLE integrations ADD COLUMN ntfy_url text default '';
|
|
||||||
ALTER TABLE integrations ADD COLUMN ntfy_topic text default '';
|
|
||||||
ALTER TABLE integrations ADD COLUMN ntfy_api_token text default '';
|
|
||||||
ALTER TABLE integrations ADD COLUMN ntfy_username text default '';
|
|
||||||
ALTER TABLE integrations ADD COLUMN ntfy_password text default '';
|
|
||||||
ALTER TABLE integrations ADD COLUMN ntfy_icon_url text default '';
|
|
||||||
|
|
||||||
ALTER TABLE feeds ADD COLUMN ntfy_enabled bool default 'f';
|
|
||||||
ALTER TABLE feeds ADD COLUMN ntfy_priority int default '3';
|
|
||||||
`
|
|
||||||
_, err = tx.Exec(sql)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
func(tx *sql.Tx) (err error) {
|
|
||||||
sql := `ALTER TABLE users ADD COLUMN mark_read_on_media_player_completion bool default 'f';`
|
|
||||||
_, err = tx.Exec(sql)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ import (
|
|||||||
"miniflux.app/v2/internal/http/request"
|
"miniflux.app/v2/internal/http/request"
|
||||||
"miniflux.app/v2/internal/http/response/json"
|
"miniflux.app/v2/internal/http/response/json"
|
||||||
"miniflux.app/v2/internal/integration"
|
"miniflux.app/v2/internal/integration"
|
||||||
"miniflux.app/v2/internal/mediaproxy"
|
|
||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
|
"miniflux.app/v2/internal/proxy"
|
||||||
"miniflux.app/v2/internal/storage"
|
"miniflux.app/v2/internal/storage"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@ -247,6 +247,7 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
|
|||||||
builder := h.store.NewEntryQueryBuilder(userID)
|
builder := h.store.NewEntryQueryBuilder(userID)
|
||||||
builder.WithoutStatus(model.EntryStatusRemoved)
|
builder.WithoutStatus(model.EntryStatusRemoved)
|
||||||
builder.WithLimit(50)
|
builder.WithLimit(50)
|
||||||
|
builder.WithSorting("id", model.DefaultSortingDirection)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case request.HasQueryParam(r, "since_id"):
|
case request.HasQueryParam(r, "since_id"):
|
||||||
@ -257,7 +258,6 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
|
|||||||
slog.Int64("since_id", sinceID),
|
slog.Int64("since_id", sinceID),
|
||||||
)
|
)
|
||||||
builder.AfterEntryID(sinceID)
|
builder.AfterEntryID(sinceID)
|
||||||
builder.WithSorting("id", "ASC")
|
|
||||||
}
|
}
|
||||||
case request.HasQueryParam(r, "max_id"):
|
case request.HasQueryParam(r, "max_id"):
|
||||||
maxID := request.QueryInt64Param(r, "max_id", 0)
|
maxID := request.QueryInt64Param(r, "max_id", 0)
|
||||||
@ -324,7 +324,7 @@ func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
|
|||||||
FeedID: entry.FeedID,
|
FeedID: entry.FeedID,
|
||||||
Title: entry.Title,
|
Title: entry.Title,
|
||||||
Author: entry.Author,
|
Author: entry.Author,
|
||||||
HTML: mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content),
|
HTML: proxy.AbsoluteProxyRewriter(h.router, r.Host, entry.Content),
|
||||||
URL: entry.URL,
|
URL: entry.URL,
|
||||||
IsSaved: isSaved,
|
IsSaved: isSaved,
|
||||||
IsRead: isRead,
|
IsRead: isRead,
|
||||||
|
@ -18,12 +18,13 @@ import (
|
|||||||
"miniflux.app/v2/internal/http/response/json"
|
"miniflux.app/v2/internal/http/response/json"
|
||||||
"miniflux.app/v2/internal/http/route"
|
"miniflux.app/v2/internal/http/route"
|
||||||
"miniflux.app/v2/internal/integration"
|
"miniflux.app/v2/internal/integration"
|
||||||
"miniflux.app/v2/internal/mediaproxy"
|
|
||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
|
"miniflux.app/v2/internal/proxy"
|
||||||
"miniflux.app/v2/internal/reader/fetcher"
|
"miniflux.app/v2/internal/reader/fetcher"
|
||||||
mff "miniflux.app/v2/internal/reader/handler"
|
mff "miniflux.app/v2/internal/reader/handler"
|
||||||
mfs "miniflux.app/v2/internal/reader/subscription"
|
mfs "miniflux.app/v2/internal/reader/subscription"
|
||||||
"miniflux.app/v2/internal/storage"
|
"miniflux.app/v2/internal/storage"
|
||||||
|
"miniflux.app/v2/internal/urllib"
|
||||||
"miniflux.app/v2/internal/validator"
|
"miniflux.app/v2/internal/validator"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@ -264,10 +265,9 @@ func getStreamFilterModifiers(r *http.Request) (RequestModifiers, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getStream(streamID string, userID int64) (Stream, error) {
|
func getStream(streamID string, userID int64) (Stream, error) {
|
||||||
switch {
|
if strings.HasPrefix(streamID, FeedPrefix) {
|
||||||
case strings.HasPrefix(streamID, FeedPrefix):
|
|
||||||
return Stream{Type: FeedStream, ID: strings.TrimPrefix(streamID, FeedPrefix)}, nil
|
return Stream{Type: FeedStream, ID: strings.TrimPrefix(streamID, FeedPrefix)}, nil
|
||||||
case strings.HasPrefix(streamID, fmt.Sprintf(UserStreamPrefix, userID)) || strings.HasPrefix(streamID, StreamPrefix):
|
} else if strings.HasPrefix(streamID, fmt.Sprintf(UserStreamPrefix, userID)) || strings.HasPrefix(streamID, StreamPrefix) {
|
||||||
id := strings.TrimPrefix(streamID, fmt.Sprintf(UserStreamPrefix, userID))
|
id := strings.TrimPrefix(streamID, fmt.Sprintf(UserStreamPrefix, userID))
|
||||||
id = strings.TrimPrefix(id, StreamPrefix)
|
id = strings.TrimPrefix(id, StreamPrefix)
|
||||||
switch id {
|
switch id {
|
||||||
@ -288,15 +288,15 @@ func getStream(streamID string, userID int64) (Stream, error) {
|
|||||||
default:
|
default:
|
||||||
return Stream{NoStream, ""}, fmt.Errorf("googlereader: unknown stream with id: %s", id)
|
return Stream{NoStream, ""}, fmt.Errorf("googlereader: unknown stream with id: %s", id)
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(streamID, fmt.Sprintf(UserLabelPrefix, userID)) || strings.HasPrefix(streamID, LabelPrefix):
|
} else if strings.HasPrefix(streamID, fmt.Sprintf(UserLabelPrefix, userID)) || strings.HasPrefix(streamID, LabelPrefix) {
|
||||||
id := strings.TrimPrefix(streamID, fmt.Sprintf(UserLabelPrefix, userID))
|
id := strings.TrimPrefix(streamID, fmt.Sprintf(UserLabelPrefix, userID))
|
||||||
id = strings.TrimPrefix(id, LabelPrefix)
|
id = strings.TrimPrefix(id, LabelPrefix)
|
||||||
return Stream{LabelStream, id}, nil
|
return Stream{LabelStream, id}, nil
|
||||||
case streamID == "":
|
} else if streamID == "" {
|
||||||
return Stream{NoStream, ""}, nil
|
return Stream{NoStream, ""}, nil
|
||||||
default:
|
|
||||||
return Stream{NoStream, ""}, fmt.Errorf("googlereader: unknown stream type: %s", streamID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Stream{NoStream, ""}, fmt.Errorf("googlereader: unknown stream type: %s", streamID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStreams(streamIDs []string, userID int64) ([]Stream, error) {
|
func getStreams(streamIDs []string, userID int64) ([]Stream, error) {
|
||||||
@ -382,7 +382,7 @@ func getItemIDs(r *http.Request) ([]int64, error) {
|
|||||||
return itemIDs, nil
|
return itemIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkOutputFormat(r *http.Request) error {
|
func checkOutputFormat(w http.ResponseWriter, r *http.Request) error {
|
||||||
var output string
|
var output string
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
@ -736,12 +736,11 @@ func getFeed(stream Stream, store *storage.Storage, userID int64) (*model.Feed,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getOrCreateCategory(category Stream, store *storage.Storage, userID int64) (*model.Category, error) {
|
func getOrCreateCategory(category Stream, store *storage.Storage, userID int64) (*model.Category, error) {
|
||||||
switch {
|
if category.ID == "" {
|
||||||
case category.ID == "":
|
|
||||||
return store.FirstCategory(userID)
|
return store.FirstCategory(userID)
|
||||||
case store.CategoryTitleExists(userID, category.ID):
|
} else if store.CategoryTitleExists(userID, category.ID) {
|
||||||
return store.CategoryByTitle(userID, category.ID)
|
return store.CategoryByTitle(userID, category.ID)
|
||||||
default:
|
} else {
|
||||||
catRequest := model.CategoryRequest{
|
catRequest := model.CategoryRequest{
|
||||||
Title: category.ID,
|
Title: category.ID,
|
||||||
}
|
}
|
||||||
@ -765,7 +764,7 @@ func subscribe(newFeed Stream, category Stream, title string, store *storage.Sto
|
|||||||
}
|
}
|
||||||
|
|
||||||
created, localizedError := mff.CreateFeed(store, userID, &feedRequest)
|
created, localizedError := mff.CreateFeed(store, userID, &feedRequest)
|
||||||
if localizedError != nil {
|
if err != nil {
|
||||||
return nil, localizedError.Error()
|
return nil, localizedError.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -909,7 +908,7 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
slog.Int64("user_id", userID),
|
slog.Int64("user_id", userID),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := checkOutputFormat(r); err != nil {
|
if err := checkOutputFormat(w, r); err != nil {
|
||||||
json.BadRequest(w, r, err)
|
json.BadRequest(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1002,18 +1001,28 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
categories = append(categories, userStarred)
|
categories = append(categories, userStarred)
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content)
|
entry.Content = proxy.AbsoluteProxyRewriter(h.router, r.Host, entry.Content)
|
||||||
|
proxyOption := config.Opts.ProxyOption()
|
||||||
|
|
||||||
entry.Enclosures.ProxifyEnclosureURL(h.router)
|
for i := range entry.Enclosures {
|
||||||
|
if proxyOption == "all" || proxyOption != "none" && !urllib.IsHTTPS(entry.Enclosures[i].URL) {
|
||||||
|
for _, mediaType := range config.Opts.ProxyMediaTypes() {
|
||||||
|
if strings.HasPrefix(entry.Enclosures[i].MimeType, mediaType+"/") {
|
||||||
|
entry.Enclosures[i].URL = proxy.AbsoluteProxifyURL(h.router, r.Host, entry.Enclosures[i].URL)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
contentItems[i] = contentItem{
|
contentItems[i] = contentItem{
|
||||||
ID: fmt.Sprintf(EntryIDLong, entry.ID),
|
ID: fmt.Sprintf(EntryIDLong, entry.ID),
|
||||||
Title: entry.Title,
|
Title: entry.Title,
|
||||||
Author: entry.Author,
|
Author: entry.Author,
|
||||||
TimestampUsec: fmt.Sprintf("%d", entry.Date.UnixMicro()),
|
TimestampUsec: fmt.Sprintf("%d", entry.Date.UnixNano()/(int64(time.Microsecond)/int64(time.Nanosecond))),
|
||||||
CrawlTimeMsec: fmt.Sprintf("%d", entry.CreatedAt.UnixMilli()),
|
CrawlTimeMsec: fmt.Sprintf("%d", entry.Date.UnixNano()/(int64(time.Microsecond)/int64(time.Nanosecond))),
|
||||||
Published: entry.Date.Unix(),
|
Published: entry.Date.Unix(),
|
||||||
Updated: entry.ChangedAt.Unix(),
|
Updated: entry.Date.Unix(),
|
||||||
Categories: categories,
|
Categories: categories,
|
||||||
Canonical: []contentHREF{
|
Canonical: []contentHREF{
|
||||||
{
|
{
|
||||||
@ -1161,7 +1170,7 @@ func (h *handler) tagListHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
slog.String("user_agent", r.UserAgent()),
|
slog.String("user_agent", r.UserAgent()),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := checkOutputFormat(r); err != nil {
|
if err := checkOutputFormat(w, r); err != nil {
|
||||||
json.BadRequest(w, r, err)
|
json.BadRequest(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1196,7 +1205,7 @@ func (h *handler) subscriptionListHandler(w http.ResponseWriter, r *http.Request
|
|||||||
slog.String("user_agent", r.UserAgent()),
|
slog.String("user_agent", r.UserAgent()),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := checkOutputFormat(r); err != nil {
|
if err := checkOutputFormat(w, r); err != nil {
|
||||||
json.BadRequest(w, r, err)
|
json.BadRequest(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1215,7 +1224,7 @@ func (h *handler) subscriptionListHandler(w http.ResponseWriter, r *http.Request
|
|||||||
URL: feed.FeedURL,
|
URL: feed.FeedURL,
|
||||||
Categories: []subscriptionCategory{{fmt.Sprintf(UserLabelPrefix, userID) + feed.Category.Title, feed.Category.Title, "folder"}},
|
Categories: []subscriptionCategory{{fmt.Sprintf(UserLabelPrefix, userID) + feed.Category.Title, feed.Category.Title, "folder"}},
|
||||||
HTMLURL: feed.SiteURL,
|
HTMLURL: feed.SiteURL,
|
||||||
IconURL: "", // TODO: Icons are base64 encoded in the DB.
|
IconURL: "", //TODO Icons are only base64 encode in DB yet
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
json.OK(w, r, result)
|
json.OK(w, r, result)
|
||||||
@ -1242,7 +1251,7 @@ func (h *handler) userInfoHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
slog.String("user_agent", r.UserAgent()),
|
slog.String("user_agent", r.UserAgent()),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := checkOutputFormat(r); err != nil {
|
if err := checkOutputFormat(w, r); err != nil {
|
||||||
json.BadRequest(w, r, err)
|
json.BadRequest(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1267,7 +1276,7 @@ func (h *handler) streamItemIDsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
slog.Int64("user_id", userID),
|
slog.Int64("user_id", userID),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := checkOutputFormat(r); err != nil {
|
if err := checkOutputFormat(w, r); err != nil {
|
||||||
json.BadRequest(w, r, err)
|
json.BadRequest(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1468,7 +1477,8 @@ func (h *handler) handleFeedStreamHandler(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
if len(rm.ExcludeTargets) > 0 {
|
if len(rm.ExcludeTargets) > 0 {
|
||||||
for _, s := range rm.ExcludeTargets {
|
for _, s := range rm.ExcludeTargets {
|
||||||
if s.Type == ReadStream {
|
switch s.Type {
|
||||||
|
case ReadStream:
|
||||||
builder.WithoutStatus(model.EntryStatusRead)
|
builder.WithoutStatus(model.EntryStatusRead)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,15 @@ package cookie // import "miniflux.app/v2/internal/http/cookie"
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miniflux.app/v2/internal/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Cookie names.
|
// Cookie names.
|
||||||
const (
|
const (
|
||||||
CookieAppSessionID = "MinifluxAppSessionID"
|
CookieAppSessionID = "MinifluxAppSessionID"
|
||||||
CookieUserSessionID = "MinifluxUserSessionID"
|
CookieUserSessionID = "MinifluxUserSessionID"
|
||||||
|
|
||||||
|
// Cookie duration in days.
|
||||||
|
cookieDuration = 30
|
||||||
)
|
)
|
||||||
|
|
||||||
// New creates a new cookie.
|
// New creates a new cookie.
|
||||||
@ -24,7 +25,7 @@ func New(name, value string, isHTTPS bool, path string) *http.Cookie {
|
|||||||
Path: basePath(path),
|
Path: basePath(path),
|
||||||
Secure: isHTTPS,
|
Secure: isHTTPS,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Expires: time.Now().Add(time.Duration(config.Opts.CleanupRemoveSessionsDays()) * 24 * time.Hour),
|
Expires: time.Now().Add(cookieDuration * 24 * time.Hour),
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,10 +37,14 @@ const (
|
|||||||
|
|
||||||
func WebAuthnSessionData(r *http.Request) *model.WebAuthnSession {
|
func WebAuthnSessionData(r *http.Request) *model.WebAuthnSession {
|
||||||
if v := r.Context().Value(WebAuthnDataContextKey); v != nil {
|
if v := r.Context().Value(WebAuthnDataContextKey); v != nil {
|
||||||
if value, valid := v.(model.WebAuthnSession); valid {
|
value, valid := v.(model.WebAuthnSession)
|
||||||
return &value
|
if !valid {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &value
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,27 +151,39 @@ func ClientIP(r *http.Request) string {
|
|||||||
|
|
||||||
func getContextStringValue(r *http.Request, key ContextKey) string {
|
func getContextStringValue(r *http.Request, key ContextKey) string {
|
||||||
if v := r.Context().Value(key); v != nil {
|
if v := r.Context().Value(key); v != nil {
|
||||||
if value, valid := v.(string); valid {
|
value, valid := v.(string)
|
||||||
return value
|
if !valid {
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func getContextBoolValue(r *http.Request, key ContextKey) bool {
|
func getContextBoolValue(r *http.Request, key ContextKey) bool {
|
||||||
if v := r.Context().Value(key); v != nil {
|
if v := r.Context().Value(key); v != nil {
|
||||||
if value, valid := v.(bool); valid {
|
value, valid := v.(bool)
|
||||||
return value
|
if !valid {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getContextInt64Value(r *http.Request, key ContextKey) int64 {
|
func getContextInt64Value(r *http.Request, key ContextKey) int64 {
|
||||||
if v := r.Context().Value(key); v != nil {
|
if v := r.Context().Value(key); v != nil {
|
||||||
if value, valid := v.(int64); valid {
|
value, valid := v.(int64)
|
||||||
return value
|
if !valid {
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/andybalholm/brotli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const compressionThreshold = 1024
|
const compressionThreshold = 1024
|
||||||
@ -98,6 +96,7 @@ func (b *Builder) Write() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) writeHeaders() {
|
func (b *Builder) writeHeaders() {
|
||||||
|
b.headers["X-XSS-Protection"] = "1; mode=block"
|
||||||
b.headers["X-Content-Type-Options"] = "nosniff"
|
b.headers["X-Content-Type-Options"] = "nosniff"
|
||||||
b.headers["X-Frame-Options"] = "DENY"
|
b.headers["X-Frame-Options"] = "DENY"
|
||||||
b.headers["Referrer-Policy"] = "no-referrer"
|
b.headers["Referrer-Policy"] = "no-referrer"
|
||||||
@ -112,15 +111,8 @@ func (b *Builder) writeHeaders() {
|
|||||||
func (b *Builder) compress(data []byte) {
|
func (b *Builder) compress(data []byte) {
|
||||||
if b.enableCompression && len(data) > compressionThreshold {
|
if b.enableCompression && len(data) > compressionThreshold {
|
||||||
acceptEncoding := b.r.Header.Get("Accept-Encoding")
|
acceptEncoding := b.r.Header.Get("Accept-Encoding")
|
||||||
switch {
|
|
||||||
case strings.Contains(acceptEncoding, "br"):
|
|
||||||
b.headers["Content-Encoding"] = "br"
|
|
||||||
b.writeHeaders()
|
|
||||||
|
|
||||||
brotliWriter := brotli.NewWriterV2(b.w, brotli.DefaultCompression)
|
switch {
|
||||||
defer brotliWriter.Close()
|
|
||||||
brotliWriter.Write(data)
|
|
||||||
return
|
|
||||||
case strings.Contains(acceptEncoding, "gzip"):
|
case strings.Contains(acceptEncoding, "gzip"):
|
||||||
b.headers["Content-Encoding"] = "gzip"
|
b.headers["Content-Encoding"] = "gzip"
|
||||||
b.writeHeaders()
|
b.writeHeaders()
|
||||||
|
@ -28,6 +28,7 @@ func TestResponseHasCommonHeaders(t *testing.T) {
|
|||||||
resp := w.Result()
|
resp := w.Result()
|
||||||
|
|
||||||
headers := map[string]string{
|
headers := map[string]string{
|
||||||
|
"X-XSS-Protection": "1; mode=block",
|
||||||
"X-Content-Type-Options": "nosniff",
|
"X-Content-Type-Options": "nosniff",
|
||||||
"X-Frame-Options": "DENY",
|
"X-Frame-Options": "DENY",
|
||||||
}
|
}
|
||||||
@ -228,34 +229,10 @@ func TestBuildResponseWithCachingAndEtag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildResponseWithBrotliCompression(t *testing.T) {
|
|
||||||
body := strings.Repeat("a", compressionThreshold+1)
|
|
||||||
r, err := http.NewRequest("GET", "/", nil)
|
|
||||||
r.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
New(w, r).WithBody(body).Write()
|
|
||||||
})
|
|
||||||
|
|
||||||
handler.ServeHTTP(w, r)
|
|
||||||
resp := w.Result()
|
|
||||||
|
|
||||||
expected := "br"
|
|
||||||
actual := resp.Header.Get("Content-Encoding")
|
|
||||||
if actual != expected {
|
|
||||||
t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildResponseWithGzipCompression(t *testing.T) {
|
func TestBuildResponseWithGzipCompression(t *testing.T) {
|
||||||
body := strings.Repeat("a", compressionThreshold+1)
|
body := strings.Repeat("a", compressionThreshold+1)
|
||||||
r, err := http.NewRequest("GET", "/", nil)
|
r, err := http.NewRequest("GET", "/", nil)
|
||||||
r.Header.Set("Accept-Encoding", "gzip, deflate")
|
r.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
package betula
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"miniflux.app/v2/internal/urllib"
|
|
||||||
"miniflux.app/v2/internal/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultClientTimeout = 10 * time.Second
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
url string
|
|
||||||
token string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(url, token string) *Client {
|
|
||||||
return &Client{url: url, token: token}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) CreateBookmark(entryURL, entryTitle string, tags []string) error {
|
|
||||||
apiEndpoint, err := urllib.JoinBaseURLAndPath(c.url, "/save-link")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("betula: unable to generate save-link endpoint: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
values := url.Values{}
|
|
||||||
values.Add("url", entryURL)
|
|
||||||
values.Add("title", entryTitle)
|
|
||||||
values.Add("tags", strings.Join(tags, ","))
|
|
||||||
|
|
||||||
request, err := http.NewRequest(http.MethodPost, apiEndpoint+"?"+values.Encode(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("betula: unable to create request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
|
||||||
request.AddCookie(&http.Cookie{Name: "betula-token", Value: c.token})
|
|
||||||
|
|
||||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
|
||||||
response, err := httpClient.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("betula: unable to send request: %v", err)
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
if response.StatusCode >= 400 {
|
|
||||||
return fmt.Errorf("betula: unable to create bookmark: url=%s status=%d", apiEndpoint, response.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"miniflux.app/v2/internal/config"
|
"miniflux.app/v2/internal/config"
|
||||||
"miniflux.app/v2/internal/integration/apprise"
|
"miniflux.app/v2/internal/integration/apprise"
|
||||||
"miniflux.app/v2/internal/integration/betula"
|
|
||||||
"miniflux.app/v2/internal/integration/espial"
|
"miniflux.app/v2/internal/integration/espial"
|
||||||
"miniflux.app/v2/internal/integration/instapaper"
|
"miniflux.app/v2/internal/integration/instapaper"
|
||||||
"miniflux.app/v2/internal/integration/linkace"
|
"miniflux.app/v2/internal/integration/linkace"
|
||||||
@ -16,12 +15,10 @@ import (
|
|||||||
"miniflux.app/v2/internal/integration/linkwarden"
|
"miniflux.app/v2/internal/integration/linkwarden"
|
||||||
"miniflux.app/v2/internal/integration/matrixbot"
|
"miniflux.app/v2/internal/integration/matrixbot"
|
||||||
"miniflux.app/v2/internal/integration/notion"
|
"miniflux.app/v2/internal/integration/notion"
|
||||||
"miniflux.app/v2/internal/integration/ntfy"
|
|
||||||
"miniflux.app/v2/internal/integration/nunuxkeeper"
|
"miniflux.app/v2/internal/integration/nunuxkeeper"
|
||||||
"miniflux.app/v2/internal/integration/omnivore"
|
"miniflux.app/v2/internal/integration/omnivore"
|
||||||
"miniflux.app/v2/internal/integration/pinboard"
|
"miniflux.app/v2/internal/integration/pinboard"
|
||||||
"miniflux.app/v2/internal/integration/pocket"
|
"miniflux.app/v2/internal/integration/pocket"
|
||||||
"miniflux.app/v2/internal/integration/raindrop"
|
|
||||||
"miniflux.app/v2/internal/integration/readeck"
|
"miniflux.app/v2/internal/integration/readeck"
|
||||||
"miniflux.app/v2/internal/integration/readwise"
|
"miniflux.app/v2/internal/integration/readwise"
|
||||||
"miniflux.app/v2/internal/integration/shaarli"
|
"miniflux.app/v2/internal/integration/shaarli"
|
||||||
@ -34,30 +31,6 @@ import (
|
|||||||
|
|
||||||
// SendEntry sends the entry to third-party providers when the user click on "Save".
|
// SendEntry sends the entry to third-party providers when the user click on "Save".
|
||||||
func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
|
func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
|
||||||
if userIntegrations.BetulaEnabled {
|
|
||||||
slog.Debug("Sending entry to Betula",
|
|
||||||
slog.Int64("user_id", userIntegrations.UserID),
|
|
||||||
slog.Int64("entry_id", entry.ID),
|
|
||||||
slog.String("entry_url", entry.URL),
|
|
||||||
)
|
|
||||||
|
|
||||||
client := betula.NewClient(userIntegrations.BetulaURL, userIntegrations.BetulaToken)
|
|
||||||
err := client.CreateBookmark(
|
|
||||||
entry.URL,
|
|
||||||
entry.Title,
|
|
||||||
entry.Tags,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Unable to send entry to Betula",
|
|
||||||
slog.Int64("user_id", userIntegrations.UserID),
|
|
||||||
slog.Int64("entry_id", entry.ID),
|
|
||||||
slog.String("entry_url", entry.URL),
|
|
||||||
slog.Any("error", err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if userIntegrations.PinboardEnabled {
|
if userIntegrations.PinboardEnabled {
|
||||||
slog.Debug("Sending entry to Pinboard",
|
slog.Debug("Sending entry to Pinboard",
|
||||||
slog.Int64("user_id", userIntegrations.UserID),
|
slog.Int64("user_id", userIntegrations.UserID),
|
||||||
@ -386,7 +359,6 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if userIntegrations.OmnivoreEnabled {
|
if userIntegrations.OmnivoreEnabled {
|
||||||
slog.Debug("Sending entry to Omnivore",
|
slog.Debug("Sending entry to Omnivore",
|
||||||
slog.Int64("user_id", userIntegrations.UserID),
|
slog.Int64("user_id", userIntegrations.UserID),
|
||||||
@ -404,24 +376,6 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if userIntegrations.RaindropEnabled {
|
|
||||||
slog.Debug("Sending entry to Raindrop",
|
|
||||||
slog.Int64("user_id", userIntegrations.UserID),
|
|
||||||
slog.Int64("entry_id", entry.ID),
|
|
||||||
slog.String("entry_url", entry.URL),
|
|
||||||
)
|
|
||||||
|
|
||||||
client := raindrop.NewClient(userIntegrations.RaindropToken, userIntegrations.RaindropCollectionID, userIntegrations.RaindropTags)
|
|
||||||
if err := client.CreateRaindrop(entry.URL, entry.Title); err != nil {
|
|
||||||
slog.Error("Unable to send entry to Raindrop",
|
|
||||||
slog.Int64("user_id", userIntegrations.UserID),
|
|
||||||
slog.Int64("entry_id", entry.ID),
|
|
||||||
slog.String("entry_url", entry.URL),
|
|
||||||
slog.Any("error", err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushEntries pushes a list of entries to activated third-party providers during feed refreshes.
|
// PushEntries pushes a list of entries to activated third-party providers during feed refreshes.
|
||||||
@ -471,28 +425,6 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if userIntegrations.NtfyEnabled && feed.NtfyEnabled {
|
|
||||||
slog.Debug("Sending new entries to Ntfy",
|
|
||||||
slog.Int64("user_id", userIntegrations.UserID),
|
|
||||||
slog.Int("nb_entries", len(entries)),
|
|
||||||
slog.Int64("feed_id", feed.ID),
|
|
||||||
)
|
|
||||||
|
|
||||||
client := ntfy.NewClient(
|
|
||||||
userIntegrations.NtfyURL,
|
|
||||||
userIntegrations.NtfyTopic,
|
|
||||||
userIntegrations.NtfyAPIToken,
|
|
||||||
userIntegrations.NtfyUsername,
|
|
||||||
userIntegrations.NtfyPassword,
|
|
||||||
userIntegrations.NtfyIconURL,
|
|
||||||
feed.NtfyPriority,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := client.SendMessages(feed, entries); err != nil {
|
|
||||||
slog.Warn("Unable to send new entries to Ntfy", slog.Any("error", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Integrations that only support sending individual entries
|
// Integrations that only support sending individual entries
|
||||||
if userIntegrations.TelegramBotEnabled || userIntegrations.AppriseEnabled {
|
if userIntegrations.TelegramBotEnabled || userIntegrations.AppriseEnabled {
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PushEntries pushes entries to matrix chat using integration settings provided
|
// PushEntry pushes entries to matrix chat using integration settings provided
|
||||||
func PushEntries(feed *model.Feed, entries model.Entries, matrixBaseURL, matrixUsername, matrixPassword, matrixRoomID string) error {
|
func PushEntries(feed *model.Feed, entries model.Entries, matrixBaseURL, matrixUsername, matrixPassword, matrixRoomID string) error {
|
||||||
client := NewClient(matrixBaseURL)
|
client := NewClient(matrixBaseURL)
|
||||||
discovery, err := client.DiscoverEndpoints()
|
discovery, err := client.DiscoverEndpoints()
|
||||||
|
@ -1,120 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package ntfy // import "miniflux.app/v2/internal/integration/ntfy"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"miniflux.app/v2/internal/model"
|
|
||||||
"miniflux.app/v2/internal/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultClientTimeout = 10 * time.Second
|
|
||||||
defaultNtfyURL = "https://ntfy.sh"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string
|
|
||||||
ntfyPriority int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL string, ntfyPriority int) *Client {
|
|
||||||
if ntfyURL == "" {
|
|
||||||
ntfyURL = defaultNtfyURL
|
|
||||||
}
|
|
||||||
return &Client{ntfyURL, ntfyTopic, ntfyApiToken, ntfyUsername, ntfyPassword, ntfyIconURL, ntfyPriority}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SendMessages(feed *model.Feed, entries model.Entries) error {
|
|
||||||
for _, entry := range entries {
|
|
||||||
ntfyMessage := &ntfyMessage{
|
|
||||||
Topic: c.ntfyTopic,
|
|
||||||
Message: entry.Title,
|
|
||||||
Title: feed.Title,
|
|
||||||
Priority: c.ntfyPriority,
|
|
||||||
Click: entry.URL,
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ntfyIconURL != "" {
|
|
||||||
ntfyMessage.Icon = c.ntfyIconURL
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Debug("Sending Ntfy message",
|
|
||||||
slog.String("url", c.ntfyURL),
|
|
||||||
slog.String("topic", c.ntfyTopic),
|
|
||||||
slog.Int("priority", ntfyMessage.Priority),
|
|
||||||
slog.String("message", ntfyMessage.Message),
|
|
||||||
slog.String("entry_url", entry.URL),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := c.makeRequest(ntfyMessage); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) makeRequest(payload any) error {
|
|
||||||
requestBody, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntfy: unable to encode request body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
request, err := http.NewRequest(http.MethodPost, c.ntfyURL, bytes.NewReader(requestBody))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntfy: unable to create request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Header.Set("Content-Type", "application/json")
|
|
||||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
|
||||||
|
|
||||||
// See https://docs.ntfy.sh/publish/#access-tokens
|
|
||||||
if c.ntfyApiToken != "" {
|
|
||||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.ntfyApiToken))
|
|
||||||
}
|
|
||||||
|
|
||||||
// See https://docs.ntfy.sh/publish/#username-password
|
|
||||||
if c.ntfyUsername != "" && c.ntfyPassword != "" {
|
|
||||||
request.SetBasicAuth(c.ntfyUsername, c.ntfyPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
|
||||||
response, err := httpClient.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ntfy: unable to send request: %v", err)
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
if response.StatusCode >= 400 {
|
|
||||||
return fmt.Errorf("ntfy: incorrect response status code %d for url %s", response.StatusCode, c.ntfyURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// See https://docs.ntfy.sh/publish/#publish-as-json
|
|
||||||
type ntfyMessage struct {
|
|
||||||
Topic string `json:"topic"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Tags []string `json:"tags,omitempty"`
|
|
||||||
Priority int `json:"priority,omitempty"`
|
|
||||||
Icon string `json:"icon,omitempty"` // https://docs.ntfy.sh/publish/#icons
|
|
||||||
Click string `json:"click,omitempty"`
|
|
||||||
Actions []ntfyAction `json:"actions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// See https://docs.ntfy.sh/publish/#action-buttons
|
|
||||||
type ntfyAction struct {
|
|
||||||
Action string `json:"action"`
|
|
||||||
Label string `json:"label"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
@ -4,8 +4,6 @@
|
|||||||
package pinboard // import "miniflux.app/v2/internal/integration/pinboard"
|
package pinboard // import "miniflux.app/v2/internal/integration/pinboard"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -14,9 +12,6 @@ import (
|
|||||||
"miniflux.app/v2/internal/version"
|
"miniflux.app/v2/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errPostNotFound = fmt.Errorf("pinboard: post not found")
|
|
||||||
var errMissingCredentials = fmt.Errorf("pinboard: missing auth token")
|
|
||||||
|
|
||||||
const defaultClientTimeout = 10 * time.Second
|
const defaultClientTimeout = 10 * time.Second
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@ -29,27 +24,20 @@ func NewClient(authToken string) *Client {
|
|||||||
|
|
||||||
func (c *Client) CreateBookmark(entryURL, entryTitle, pinboardTags string, markAsUnread bool) error {
|
func (c *Client) CreateBookmark(entryURL, entryTitle, pinboardTags string, markAsUnread bool) error {
|
||||||
if c.authToken == "" {
|
if c.authToken == "" {
|
||||||
return errMissingCredentials
|
return fmt.Errorf("pinboard: missing auth token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// We check if the url is already bookmarked to avoid overriding existing data.
|
toRead := "no"
|
||||||
post, err := c.getBookmark(entryURL)
|
|
||||||
|
|
||||||
if err != nil && errors.Is(err, errPostNotFound) {
|
|
||||||
post = NewPost(entryURL, entryTitle)
|
|
||||||
} else if err != nil {
|
|
||||||
// In case of any other error, we return immediately to avoid overriding existing data.
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
post.addTag(pinboardTags)
|
|
||||||
if markAsUnread {
|
if markAsUnread {
|
||||||
post.SetToread()
|
toRead = "yes"
|
||||||
}
|
}
|
||||||
|
|
||||||
values := url.Values{}
|
values := url.Values{}
|
||||||
values.Add("auth_token", c.authToken)
|
values.Add("auth_token", c.authToken)
|
||||||
post.AddValues(values)
|
values.Add("url", entryURL)
|
||||||
|
values.Add("description", entryTitle)
|
||||||
|
values.Add("tags", pinboardTags)
|
||||||
|
values.Add("toread", toRead)
|
||||||
|
|
||||||
apiEndpoint := "https://api.pinboard.in/v1/posts/add?" + values.Encode()
|
apiEndpoint := "https://api.pinboard.in/v1/posts/add?" + values.Encode()
|
||||||
request, err := http.NewRequest(http.MethodGet, apiEndpoint, nil)
|
request, err := http.NewRequest(http.MethodGet, apiEndpoint, nil)
|
||||||
@ -73,46 +61,3 @@ func (c *Client) CreateBookmark(entryURL, entryTitle, pinboardTags string, markA
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getBookmark fetches a bookmark from Pinboard. https://www.pinboard.in/api/#posts_get
|
|
||||||
func (c *Client) getBookmark(entryURL string) (*Post, error) {
|
|
||||||
if c.authToken == "" {
|
|
||||||
return nil, errMissingCredentials
|
|
||||||
}
|
|
||||||
|
|
||||||
values := url.Values{}
|
|
||||||
values.Add("auth_token", c.authToken)
|
|
||||||
values.Add("url", entryURL)
|
|
||||||
|
|
||||||
apiEndpoint := "https://api.pinboard.in/v1/posts/get?" + values.Encode()
|
|
||||||
request, err := http.NewRequest(http.MethodGet, apiEndpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("pinboard: unable to create request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
|
||||||
|
|
||||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
|
||||||
response, err := httpClient.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("pinboard: unable fetch bookmark: %v", err)
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
if response.StatusCode >= 400 {
|
|
||||||
return nil, fmt.Errorf("pinboard: unable to fetch bookmark, status=%d", response.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var results posts
|
|
||||||
err = xml.NewDecoder(response.Body).Decode(&results)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("pinboard: unable to decode XML: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results.Posts) == 0 {
|
|
||||||
return nil, errPostNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return &results.Posts[0], nil
|
|
||||||
}
|
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package pinboard // import "miniflux.app/v2/internal/integration/pinboard"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Post a Pinboard bookmark. "inspiration" from https://github.com/drags/pinboard/blob/master/posts.go#L32-L42
|
|
||||||
type Post struct {
|
|
||||||
XMLName xml.Name `xml:"post"`
|
|
||||||
Url string `xml:"href,attr"`
|
|
||||||
Description string `xml:"description,attr"`
|
|
||||||
Tags string `xml:"tag,attr"`
|
|
||||||
Extended string `xml:"extended,attr"`
|
|
||||||
Date time.Time `xml:"time,attr"`
|
|
||||||
Shared string `xml:"shared,attr"`
|
|
||||||
Toread string `xml:"toread,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Posts A result of a Pinboard API call
|
|
||||||
type posts struct {
|
|
||||||
XMLName xml.Name `xml:"posts"`
|
|
||||||
Posts []Post `xml:"post"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPost(url string, description string) *Post {
|
|
||||||
return &Post{
|
|
||||||
Url: url,
|
|
||||||
Description: description,
|
|
||||||
Date: time.Now(),
|
|
||||||
Toread: "no",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Post) addTag(tag string) {
|
|
||||||
if !strings.Contains(p.Tags, tag) {
|
|
||||||
p.Tags += " " + tag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Post) SetToread() {
|
|
||||||
p.Toread = "yes"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Post) AddValues(values url.Values) {
|
|
||||||
values.Add("url", p.Url)
|
|
||||||
values.Add("description", p.Description)
|
|
||||||
values.Add("tags", p.Tags)
|
|
||||||
if p.Toread != "" {
|
|
||||||
values.Add("toread", p.Toread)
|
|
||||||
}
|
|
||||||
if p.Shared != "" {
|
|
||||||
values.Add("shared", p.Shared)
|
|
||||||
}
|
|
||||||
values.Add("dt", p.Date.Format(time.RFC3339))
|
|
||||||
values.Add("extended", p.Extended)
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package raindrop // import "miniflux.app/v2/internal/integration/raindrop"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"miniflux.app/v2/internal/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultClientTimeout = 10 * time.Second
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
token string
|
|
||||||
collectionID string
|
|
||||||
tags []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(token, collectionID, tags string) *Client {
|
|
||||||
return &Client{token: token, collectionID: collectionID, tags: strings.Split(tags, ",")}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://developer.raindrop.io/v1/raindrops/single#create-raindrop
|
|
||||||
func (c *Client) CreateRaindrop(entryURL, entryTitle string) error {
|
|
||||||
if c.token == "" {
|
|
||||||
return fmt.Errorf("raindrop: missing token")
|
|
||||||
}
|
|
||||||
|
|
||||||
var request *http.Request
|
|
||||||
requestBodyJson, err := json.Marshal(&raindrop{
|
|
||||||
Link: entryURL,
|
|
||||||
Title: entryTitle,
|
|
||||||
Collection: collection{Id: c.collectionID},
|
|
||||||
Tags: c.tags,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("raindrop: unable to encode request body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
request, err = http.NewRequest(http.MethodPost, "https://api.raindrop.io/rest/v1/raindrop", bytes.NewReader(requestBodyJson))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("raindrop: unable to create request: %v", err)
|
|
||||||
}
|
|
||||||
request.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
request.Header.Set("User-Agent", "Miniflux/"+version.Version)
|
|
||||||
request.Header.Set("Authorization", "Bearer "+c.token)
|
|
||||||
|
|
||||||
httpClient := &http.Client{Timeout: defaultClientTimeout}
|
|
||||||
response, err := httpClient.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("raindrop: unable to send request: %v", err)
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
if response.StatusCode >= 400 {
|
|
||||||
return fmt.Errorf("raindrop: unable to create bookmark: status=%d", response.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type raindrop struct {
|
|
||||||
Link string `json:"link"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Collection collection `json:"collection,omitempty"`
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type collection struct {
|
|
||||||
Id string `json:"$id"`
|
|
||||||
}
|
|
@ -91,7 +91,7 @@ func (c *Client) CreateBookmark(entryURL, entryTitle string, entryContent string
|
|||||||
|
|
||||||
contentBodyHeader, err := json.Marshal(&partContentHeader{
|
contentBodyHeader, err := json.Marshal(&partContentHeader{
|
||||||
Url: entryURL,
|
Url: entryURL,
|
||||||
ContentHeader: contentHeader{ContentType: "text/html; charset=utf-8"},
|
ContentHeader: contentHeader{ContentType: "text/html"},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("readeck: unable to encode request body (entry content header): %v", err)
|
return fmt.Errorf("readeck: unable to encode request body (entry content header): %v", err)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package rssbridge // import "miniflux.app/v2/internal/integration/rssbridge"
|
package rssbridge // import "miniflux.app/integration/rssbridge"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miniflux.app/v2/internal/urllib"
|
"miniflux.app/v2/internal/urllib"
|
||||||
@ -73,15 +74,14 @@ func (c *Client) CreateLink(entryURL, entryTitle string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) generateBearerToken() string {
|
func (c *Client) generateBearerToken() string {
|
||||||
header := base64.RawURLEncoding.EncodeToString([]byte(`{"typ":"JWT","alg":"HS512"}`))
|
header := strings.TrimRight(base64.URLEncoding.EncodeToString([]byte(`{"typ":"JWT", "alg":"HS256"}`)), "=")
|
||||||
payload := base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf(`{"iat":%d}`, time.Now().Unix())))
|
payload := strings.TrimRight(base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf(`{"iat": %d}`, time.Now().Unix()))), "=")
|
||||||
data := header + "." + payload
|
|
||||||
|
|
||||||
mac := hmac.New(sha512.New, []byte(c.apiSecret))
|
mac := hmac.New(sha512.New, []byte(c.apiSecret))
|
||||||
mac.Write([]byte(data))
|
mac.Write([]byte(header + "." + payload))
|
||||||
signature := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
signature := strings.TrimRight(base64.URLEncoding.EncodeToString(mac.Sum(nil)), "=")
|
||||||
|
|
||||||
return data + "." + signature
|
return header + "." + payload + "." + signature
|
||||||
}
|
}
|
||||||
|
|
||||||
type addLinkRequest struct {
|
type addLinkRequest struct {
|
||||||
|
@ -11,8 +11,7 @@ import (
|
|||||||
|
|
||||||
func PushEntry(feed *model.Feed, entry *model.Entry, botToken, chatID string, topicID *int64, disableWebPagePreview, disableNotification bool, disableButtons bool) error {
|
func PushEntry(feed *model.Feed, entry *model.Entry, botToken, chatID string, topicID *int64, disableWebPagePreview, disableNotification bool, disableButtons bool) error {
|
||||||
formattedText := fmt.Sprintf(
|
formattedText := fmt.Sprintf(
|
||||||
`<b>%s</b> - <a href=%q>%s</a>`,
|
`<a href=%q>%s</a>`,
|
||||||
feed.Title,
|
|
||||||
entry.URL,
|
entry.URL,
|
||||||
entry.Title,
|
entry.Title,
|
||||||
)
|
)
|
||||||
|
@ -57,7 +57,6 @@ func (c *Client) SendSaveEntryWebhookEvent(entry *model.Entry) error {
|
|||||||
ID: entry.Feed.ID,
|
ID: entry.Feed.ID,
|
||||||
UserID: entry.Feed.UserID,
|
UserID: entry.Feed.UserID,
|
||||||
CategoryID: entry.Feed.Category.ID,
|
CategoryID: entry.Feed.Category.ID,
|
||||||
Category: &WebhookCategory{ID: entry.Feed.Category.ID, Title: entry.Feed.Category.Title},
|
|
||||||
FeedURL: entry.Feed.FeedURL,
|
FeedURL: entry.Feed.FeedURL,
|
||||||
SiteURL: entry.Feed.SiteURL,
|
SiteURL: entry.Feed.SiteURL,
|
||||||
Title: entry.Feed.Title,
|
Title: entry.Feed.Title,
|
||||||
@ -95,13 +94,13 @@ func (c *Client) SendNewEntriesWebhookEvent(feed *model.Feed, entries model.Entr
|
|||||||
Tags: entry.Tags,
|
Tags: entry.Tags,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.makeRequest(NewEntriesEventType, &WebhookNewEntriesEvent{
|
return c.makeRequest(NewEntriesEventType, &WebhookNewEntriesEvent{
|
||||||
EventType: NewEntriesEventType,
|
EventType: NewEntriesEventType,
|
||||||
Feed: &WebhookFeed{
|
Feed: &WebhookFeed{
|
||||||
ID: feed.ID,
|
ID: feed.ID,
|
||||||
UserID: feed.UserID,
|
UserID: feed.UserID,
|
||||||
CategoryID: feed.Category.ID,
|
CategoryID: feed.Category.ID,
|
||||||
Category: &WebhookCategory{ID: feed.Category.ID, Title: feed.Category.Title},
|
|
||||||
FeedURL: feed.FeedURL,
|
FeedURL: feed.FeedURL,
|
||||||
SiteURL: feed.SiteURL,
|
SiteURL: feed.SiteURL,
|
||||||
Title: feed.Title,
|
Title: feed.Title,
|
||||||
@ -146,19 +145,13 @@ func (c *Client) makeRequest(eventType string, payload any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WebhookFeed struct {
|
type WebhookFeed struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
CategoryID int64 `json:"category_id"`
|
CategoryID int64 `json:"category_id"`
|
||||||
Category *WebhookCategory `json:"category,omitempty"`
|
FeedURL string `json:"feed_url"`
|
||||||
FeedURL string `json:"feed_url"`
|
SiteURL string `json:"site_url"`
|
||||||
SiteURL string `json:"site_url"`
|
Title string `json:"title"`
|
||||||
Title string `json:"title"`
|
CheckedAt time.Time `json:"checked_at"`
|
||||||
CheckedAt time.Time `json:"checked_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebhookCategory struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebhookEntry struct {
|
type WebhookEntry struct {
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "Alle als gelesen markieren",
|
"menu.mark_all_as_read": "Alle als gelesen markieren",
|
||||||
"menu.show_all_entries": "Zeige alle Artikel",
|
"menu.show_all_entries": "Zeige alle Artikel",
|
||||||
"menu.show_only_unread_entries": "Nur ungelesene Artikel anzeigen",
|
"menu.show_only_unread_entries": "Nur ungelesene Artikel anzeigen",
|
||||||
"menu.show_only_starred_entries": "Nur markierte Artikel anzeigen",
|
|
||||||
"menu.refresh_feed": "Aktualisieren",
|
"menu.refresh_feed": "Aktualisieren",
|
||||||
"menu.refresh_all_feeds": "Alle Abonnements im Hintergrund aktualisieren",
|
"menu.refresh_all_feeds": "Alle Abonnements im Hintergrund aktualisieren",
|
||||||
"menu.edit_feed": "Bearbeiten",
|
"menu.edit_feed": "Bearbeiten",
|
||||||
@ -56,9 +55,7 @@
|
|||||||
"search.label": "Suche",
|
"search.label": "Suche",
|
||||||
"search.placeholder": "Suche...",
|
"search.placeholder": "Suche...",
|
||||||
"search.submit": "Search",
|
"search.submit": "Search",
|
||||||
"pagination.last": "Last",
|
|
||||||
"pagination.next": "Nächste",
|
"pagination.next": "Nächste",
|
||||||
"pagination.first": "First",
|
|
||||||
"pagination.previous": "Vorherige",
|
"pagination.previous": "Vorherige",
|
||||||
"entry.status.unread": "Ungelesen",
|
"entry.status.unread": "Ungelesen",
|
||||||
"entry.status.read": "Gelesen",
|
"entry.status.read": "Gelesen",
|
||||||
@ -181,8 +178,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "Zum Abonnement gehen",
|
"page.keyboard_shortcuts.go_to_feed": "Zum Abonnement gehen",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "Zur vorherigen Seite gehen",
|
"page.keyboard_shortcuts.go_to_previous_page": "Zur vorherigen Seite gehen",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "Zur nächsten Seite gehen",
|
"page.keyboard_shortcuts.go_to_next_page": "Zur nächsten Seite gehen",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "Gehen Sie zum untersten Element",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "Zum obersten Artikel gehen",
|
|
||||||
"page.keyboard_shortcuts.open_item": "Gewählten Artikel öffnen",
|
"page.keyboard_shortcuts.open_item": "Gewählten Artikel öffnen",
|
||||||
"page.keyboard_shortcuts.open_original": "Original-Artikel öffnen",
|
"page.keyboard_shortcuts.open_original": "Original-Artikel öffnen",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "Öffne den Original-Link in der aktuellen Registerkarte",
|
"page.keyboard_shortcuts.open_original_same_window": "Öffne den Original-Link in der aktuellen Registerkarte",
|
||||||
@ -211,8 +206,8 @@
|
|||||||
"page.settings.title": "Einstellungen",
|
"page.settings.title": "Einstellungen",
|
||||||
"page.settings.link_google_account": "Google-Konto verknüpfen",
|
"page.settings.link_google_account": "Google-Konto verknüpfen",
|
||||||
"page.settings.unlink_google_account": "Verknüpfung mit Google-Konto entfernen",
|
"page.settings.unlink_google_account": "Verknüpfung mit Google-Konto entfernen",
|
||||||
"page.settings.link_oidc_account": "%s-Konto verknüpfen",
|
"page.settings.link_oidc_account": "OpenID-Connect-Konto verknüpfen",
|
||||||
"page.settings.unlink_oidc_account": "Verknüpfung mit %s-Konto entfernen",
|
"page.settings.unlink_oidc_account": "Verknüpfung mit OpenID-Connect-Konto entfernen",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Aktionen",
|
"page.settings.webauthn.actions": "Aktionen",
|
||||||
"page.settings.webauthn.passkey_name": "Name des Passkeys",
|
"page.settings.webauthn.passkey_name": "Name des Passkeys",
|
||||||
@ -226,7 +221,7 @@
|
|||||||
],
|
],
|
||||||
"page.login.title": "Anmeldung",
|
"page.login.title": "Anmeldung",
|
||||||
"page.login.google_signin": "Anmeldung mit Google",
|
"page.login.google_signin": "Anmeldung mit Google",
|
||||||
"page.login.oidc_signin": "Anmeldung mit %s",
|
"page.login.oidc_signin": "Anmeldung mit OpenID Connect",
|
||||||
"page.login.webauthn_login": "Melden Sie sich mit dem Passkey an",
|
"page.login.webauthn_login": "Melden Sie sich mit dem Passkey an",
|
||||||
"page.login.webauthn_login.error": "Anmeldung mit Passkey nicht möglich",
|
"page.login.webauthn_login.error": "Anmeldung mit Passkey nicht möglich",
|
||||||
"page.integrations.title": "Dienste",
|
"page.integrations.title": "Dienste",
|
||||||
@ -261,7 +256,6 @@
|
|||||||
"alert.no_bookmark": "Es existiert derzeit kein Lesezeichen.",
|
"alert.no_bookmark": "Es existiert derzeit kein Lesezeichen.",
|
||||||
"alert.no_category": "Es ist keine Kategorie vorhanden.",
|
"alert.no_category": "Es ist keine Kategorie vorhanden.",
|
||||||
"alert.no_category_entry": "Es befindet sich kein Artikel in dieser Kategorie.",
|
"alert.no_category_entry": "Es befindet sich kein Artikel in dieser Kategorie.",
|
||||||
"alert.no_tag_entry": "Es gibt keine Artikel, die diesem Tag entsprechen.",
|
|
||||||
"alert.no_feed_entry": "Es existiert kein Artikel für dieses Abonnement.",
|
"alert.no_feed_entry": "Es existiert kein Artikel für dieses Abonnement.",
|
||||||
"alert.no_feed": "Es sind keine Abonnements vorhanden.",
|
"alert.no_feed": "Es sind keine Abonnements vorhanden.",
|
||||||
"alert.no_feed_in_category": "Für diese Kategorie gibt es kein Abonnement.",
|
"alert.no_feed_in_category": "Für diese Kategorie gibt es kein Abonnement.",
|
||||||
@ -303,14 +297,6 @@
|
|||||||
"error.password_min_length": "Wenigstens 6 Zeichen müssen genutzt werden.",
|
"error.password_min_length": "Wenigstens 6 Zeichen müssen genutzt werden.",
|
||||||
"error.settings_mandatory_fields": "Die Felder für Benutzername, Thema, Sprache und Zeitzone sind obligatorisch.",
|
"error.settings_mandatory_fields": "Die Felder für Benutzername, Thema, Sprache und Zeitzone sind obligatorisch.",
|
||||||
"error.settings_reading_speed_is_positive": "Die Lesegeschwindigkeiten müssen positive ganze Zahlen sein.",
|
"error.settings_reading_speed_is_positive": "Die Lesegeschwindigkeiten müssen positive ganze Zahlen sein.",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.entries_per_page_invalid": "Die Anzahl der Einträge pro Seite ist ungültig.",
|
"error.entries_per_page_invalid": "Die Anzahl der Einträge pro Seite ist ungültig.",
|
||||||
"error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.",
|
"error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.",
|
||||||
"error.feed_already_exists": "Dieser Feed existiert bereits.",
|
"error.feed_already_exists": "Dieser Feed existiert bereits.",
|
||||||
@ -328,7 +314,6 @@
|
|||||||
"form.feed.label.title": "Titel",
|
"form.feed.label.title": "Titel",
|
||||||
"form.feed.label.site_url": "URL der Webseite",
|
"form.feed.label.site_url": "URL der Webseite",
|
||||||
"form.feed.label.feed_url": "URL des Abonnements",
|
"form.feed.label.feed_url": "URL des Abonnements",
|
||||||
"form.feed.label.description": "Beschreibung",
|
|
||||||
"form.feed.label.category": "Kategorie",
|
"form.feed.label.category": "Kategorie",
|
||||||
"form.feed.label.crawler": "Originalinhalt herunterladen",
|
"form.feed.label.crawler": "Originalinhalt herunterladen",
|
||||||
"form.feed.label.feed_username": "Benutzername des Abonnements",
|
"form.feed.label.feed_username": "Benutzername des Abonnements",
|
||||||
@ -348,13 +333,6 @@
|
|||||||
"form.feed.label.disabled": "Dieses Abonnement nicht aktualisieren",
|
"form.feed.label.disabled": "Dieses Abonnement nicht aktualisieren",
|
||||||
"form.feed.label.no_media_player": "Kein Media-Player (Audio/Video)",
|
"form.feed.label.no_media_player": "Kein Media-Player (Audio/Video)",
|
||||||
"form.feed.label.hide_globally": "Einträge in der globalen Ungelesen-Liste ausblenden",
|
"form.feed.label.hide_globally": "Einträge in der globalen Ungelesen-Liste ausblenden",
|
||||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
|
||||||
"form.feed.fieldset.general": "Allgemein",
|
"form.feed.fieldset.general": "Allgemein",
|
||||||
"form.feed.fieldset.rules": "Regeln",
|
"form.feed.fieldset.rules": "Regeln",
|
||||||
"form.feed.fieldset.network_settings": "Netzwerkeinstellungen",
|
"form.feed.fieldset.network_settings": "Netzwerkeinstellungen",
|
||||||
@ -395,18 +373,11 @@
|
|||||||
"form.prefs.label.default_home_page": "Standard-Startseite",
|
"form.prefs.label.default_home_page": "Standard-Startseite",
|
||||||
"form.prefs.label.categories_sorting_order": "Kategorie-Sortierung",
|
"form.prefs.label.categories_sorting_order": "Kategorie-Sortierung",
|
||||||
"form.prefs.label.mark_read_on_view": "Einträge automatisch als gelesen markieren, wenn sie angezeigt werden",
|
"form.prefs.label.mark_read_on_view": "Einträge automatisch als gelesen markieren, wenn sie angezeigt werden",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
|
||||||
"form.prefs.fieldset.application_settings": "Anwendungseinstellungen",
|
"form.prefs.fieldset.application_settings": "Anwendungseinstellungen",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentifizierungseinstellungen",
|
"form.prefs.fieldset.authentication_settings": "Authentifizierungseinstellungen",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader-Einstellungen",
|
"form.prefs.fieldset.reader_settings": "Reader-Einstellungen",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
|
||||||
"form.import.label.file": "OPML Datei",
|
"form.import.label.file": "OPML Datei",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "Save entries to Betula",
|
|
||||||
"form.integration.betula_url": "Betula server URL",
|
|
||||||
"form.integration.betula_token": "Betula Token",
|
|
||||||
"form.integration.fever_activate": "Fever API aktivieren",
|
"form.integration.fever_activate": "Fever API aktivieren",
|
||||||
"form.integration.fever_username": "Fever Benutzername",
|
"form.integration.fever_username": "Fever Benutzername",
|
||||||
"form.integration.fever_password": "Fever Passwort",
|
"form.integration.fever_password": "Fever Passwort",
|
||||||
@ -478,10 +449,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Passwort für Matrix-Benutzer",
|
"form.integration.matrix_bot_password": "Passwort für Matrix-Benutzer",
|
||||||
"form.integration.matrix_bot_url": "URL des Matrix-Servers",
|
"form.integration.matrix_bot_url": "URL des Matrix-Servers",
|
||||||
"form.integration.matrix_bot_chat_id": "ID des Matrix-Raums",
|
"form.integration.matrix_bot_chat_id": "ID des Matrix-Raums",
|
||||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "Artikel in Readeck speichern",
|
"form.integration.readeck_activate": "Artikel in Readeck speichern",
|
||||||
"form.integration.readeck_endpoint": "Readeck API-Endpunkt",
|
"form.integration.readeck_endpoint": "Readeck API-Endpunkt",
|
||||||
"form.integration.readeck_api_key": "Readeck API-Schlüssel",
|
"form.integration.readeck_api_key": "Readeck API-Schlüssel",
|
||||||
@ -499,13 +466,6 @@
|
|||||||
"form.integration.webhook_secret": "Webhook Geheimnis",
|
"form.integration.webhook_secret": "Webhook Geheimnis",
|
||||||
"form.integration.rssbridge_activate": "Beim Hinzufügen von Abonnements RSS-Bridge prüfen.",
|
"form.integration.rssbridge_activate": "Beim Hinzufügen von Abonnements RSS-Bridge prüfen.",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
|
||||||
"form.api_key.label.description": "API-Schlüsselbezeichnung",
|
"form.api_key.label.description": "API-Schlüsselbezeichnung",
|
||||||
"form.submit.loading": "Lade...",
|
"form.submit.loading": "Lade...",
|
||||||
"form.submit.saving": "Speichern...",
|
"form.submit.saving": "Speichern...",
|
||||||
@ -545,7 +505,7 @@
|
|||||||
"error.http_body_read": "Der HTTP-Inhalt kann nicht gelesen werden: %v",
|
"error.http_body_read": "Der HTTP-Inhalt kann nicht gelesen werden: %v",
|
||||||
"error.http_empty_response_body": "Der Inhalt der HTTP-Antwort ist leer.",
|
"error.http_empty_response_body": "Der Inhalt der HTTP-Antwort ist leer.",
|
||||||
"error.http_empty_response": "Die HTTP-Antwort ist leer. Vielleicht versucht die Webseite, sich vor Bots zu schützen?",
|
"error.http_empty_response": "Die HTTP-Antwort ist leer. Vielleicht versucht die Webseite, sich vor Bots zu schützen?",
|
||||||
"error.tls_error": "TLS-Fehler: %q. Wenn Sie mögen, können Sie versuchen die TLS-Verifizierung in den Einstellungen des Abonnements zu deaktivieren.",
|
"error.tls_error": "TLS-Fehler: %v. Wenn Sie mögen, können Sie versuchen die TLS-Verifizierung in den Einstellungen des Abonnements zu deaktivieren.",
|
||||||
"error.network_operation": "Miniflux kann die Webseite aufgrund eines Netzwerk-Fehlers nicht erreichen: %v",
|
"error.network_operation": "Miniflux kann die Webseite aufgrund eines Netzwerk-Fehlers nicht erreichen: %v",
|
||||||
"error.network_timeout": "Die Webseite ist zu langsam und die Anfrage ist abgelaufen: %v.",
|
"error.network_timeout": "Die Webseite ist zu langsam und die Anfrage ist abgelaufen: %v.",
|
||||||
"error.http_client_error": "HTTP-Client-Fehler: %v.",
|
"error.http_client_error": "HTTP-Client-Fehler: %v.",
|
||||||
@ -564,16 +524,5 @@
|
|||||||
"error.unable_to_parse_feed": "Dieses Abonnement kann nicht gelesen werden: %v.",
|
"error.unable_to_parse_feed": "Dieses Abonnement kann nicht gelesen werden: %v.",
|
||||||
"error.feed_not_found": "Dieses Abonnement existiert nicht oder gehört nicht zu diesem Benutzer.",
|
"error.feed_not_found": "Dieses Abonnement existiert nicht oder gehört nicht zu diesem Benutzer.",
|
||||||
"error.unable_to_detect_rssbridge": "Abonnement kann nicht durch RSS-Bridge erkannt werden: %v.",
|
"error.unable_to_detect_rssbridge": "Abonnement kann nicht durch RSS-Bridge erkannt werden: %v.",
|
||||||
"error.feed_format_not_detected": "Das Format des Abonnements kann nicht erkannt werden: %v.",
|
"error.feed_format_not_detected": "Das Format des Abonnements kann nicht erkannt werden: %v."
|
||||||
"form.prefs.label.media_playback_rate": "Wiedergabegeschwindigkeit von Audio/Video",
|
|
||||||
"error.settings_media_playback_rate_range": "Die Wiedergabegeschwindigkeit liegt außerhalb des Bereichs",
|
|
||||||
"enclosure_media_controls.seek" : "Seek:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Speed:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Faster",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Slower",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "Σημείωση όλων ως αναγνωσμένα",
|
"menu.mark_all_as_read": "Σημείωση όλων ως αναγνωσμένα",
|
||||||
"menu.show_all_entries": "Εμφάνιση όλων των καταχωρήσεων",
|
"menu.show_all_entries": "Εμφάνιση όλων των καταχωρήσεων",
|
||||||
"menu.show_only_unread_entries": "Εμφάνιση μόνο μη αναγνωσμένων καταχωρήσεων",
|
"menu.show_only_unread_entries": "Εμφάνιση μόνο μη αναγνωσμένων καταχωρήσεων",
|
||||||
"menu.show_only_starred_entries": "Εμφάνιση μόνο αγαπημένων καταχωρήσεων",
|
|
||||||
"menu.refresh_feed": "Ανανέωση",
|
"menu.refresh_feed": "Ανανέωση",
|
||||||
"menu.refresh_all_feeds": "Ανανέωση όλων των ροών στο παρασκήνιο",
|
"menu.refresh_all_feeds": "Ανανέωση όλων των ροών στο παρασκήνιο",
|
||||||
"menu.edit_feed": "Επεξεργασία",
|
"menu.edit_feed": "Επεξεργασία",
|
||||||
@ -56,9 +55,7 @@
|
|||||||
"search.label": "Αναζήτηση",
|
"search.label": "Αναζήτηση",
|
||||||
"search.placeholder": "Αναζήτηση...",
|
"search.placeholder": "Αναζήτηση...",
|
||||||
"search.submit": "Search",
|
"search.submit": "Search",
|
||||||
"pagination.last": "Last",
|
|
||||||
"pagination.next": "Επόμενη",
|
"pagination.next": "Επόμενη",
|
||||||
"pagination.first": "First",
|
|
||||||
"pagination.previous": "Προηγούμενη",
|
"pagination.previous": "Προηγούμενη",
|
||||||
"entry.status.unread": "Μη αναγνωσμένο",
|
"entry.status.unread": "Μη αναγνωσμένο",
|
||||||
"entry.status.read": "Αναγνωσμένο",
|
"entry.status.read": "Αναγνωσμένο",
|
||||||
@ -181,8 +178,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "Πηγαίνετε στη ροή",
|
"page.keyboard_shortcuts.go_to_feed": "Πηγαίνετε στη ροή",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "Μετάβαση στην προηγούμενη σελίδα",
|
"page.keyboard_shortcuts.go_to_previous_page": "Μετάβαση στην προηγούμενη σελίδα",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "Μετάβαση στην επόμενη σελίδα",
|
"page.keyboard_shortcuts.go_to_next_page": "Μετάβαση στην επόμενη σελίδα",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "Μετάβαση στο κάτω στοιχείο",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "Μετάβαση στο επάνω στοιχείο",
|
|
||||||
"page.keyboard_shortcuts.open_item": "Άνοιγμα επιλεγμένου στοιχείου",
|
"page.keyboard_shortcuts.open_item": "Άνοιγμα επιλεγμένου στοιχείου",
|
||||||
"page.keyboard_shortcuts.open_original": "Άνοιγμα αρχικού συνδέσμου",
|
"page.keyboard_shortcuts.open_original": "Άνοιγμα αρχικού συνδέσμου",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "Άνοιγμα αρχικού συνδέσμου στην τρέχουσα καρτέλα",
|
"page.keyboard_shortcuts.open_original_same_window": "Άνοιγμα αρχικού συνδέσμου στην τρέχουσα καρτέλα",
|
||||||
@ -211,8 +206,8 @@
|
|||||||
"page.settings.title": "Ρυθμίσεις",
|
"page.settings.title": "Ρυθμίσεις",
|
||||||
"page.settings.link_google_account": "Σύνδεση του λογαριασμό μου Google",
|
"page.settings.link_google_account": "Σύνδεση του λογαριασμό μου Google",
|
||||||
"page.settings.unlink_google_account": "Αποσύνδεση του λογαριασμού μου Google",
|
"page.settings.unlink_google_account": "Αποσύνδεση του λογαριασμού μου Google",
|
||||||
"page.settings.link_oidc_account": "Σύνδεση του λογαριασμού μου %s",
|
"page.settings.link_oidc_account": "Σύνδεση του λογαριασμού μου OpenID Connect",
|
||||||
"page.settings.unlink_oidc_account": "Αποσύνδεση του λογαριασμού μου %s",
|
"page.settings.unlink_oidc_account": "Αποσύνδεση του λογαριασμού μου OpenID Connect",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Actions",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||||
@ -226,7 +221,7 @@
|
|||||||
],
|
],
|
||||||
"page.login.title": "Είσοδος",
|
"page.login.title": "Είσοδος",
|
||||||
"page.login.google_signin": "Συνδεθείτε με τo Google",
|
"page.login.google_signin": "Συνδεθείτε με τo Google",
|
||||||
"page.login.oidc_signin": "Συνδεθείτε με το %s",
|
"page.login.oidc_signin": "Συνδεθείτε με το OpenID Connect",
|
||||||
"page.login.webauthn_login": "Είσοδος με κωδικό πρόσβασης",
|
"page.login.webauthn_login": "Είσοδος με κωδικό πρόσβασης",
|
||||||
"page.login.webauthn_login.error": "Δεν είναι δυνατή η σύνδεση με κωδικό πρόσβασης",
|
"page.login.webauthn_login.error": "Δεν είναι δυνατή η σύνδεση με κωδικό πρόσβασης",
|
||||||
"page.integrations.title": "Ενσωμάτωση",
|
"page.integrations.title": "Ενσωμάτωση",
|
||||||
@ -261,7 +256,6 @@
|
|||||||
"alert.no_bookmark": "Δεν υπάρχει σελιδοδείκτης αυτή τη στιγμή.",
|
"alert.no_bookmark": "Δεν υπάρχει σελιδοδείκτης αυτή τη στιγμή.",
|
||||||
"alert.no_category": "Δεν υπάρχει κατηγορία.",
|
"alert.no_category": "Δεν υπάρχει κατηγορία.",
|
||||||
"alert.no_category_entry": "Δεν υπάρχουν άρθρα σε αυτήν την κατηγορία.",
|
"alert.no_category_entry": "Δεν υπάρχουν άρθρα σε αυτήν την κατηγορία.",
|
||||||
"alert.no_tag_entry": "Δεν υπάρχουν αντικείμενα που να ταιριάζουν με αυτή την ετικέτα.",
|
|
||||||
"alert.no_feed_entry": "Δεν υπάρχουν άρθρα για αυτήν τη ροή.",
|
"alert.no_feed_entry": "Δεν υπάρχουν άρθρα για αυτήν τη ροή.",
|
||||||
"alert.no_feed": "Δεν έχετε συνδρομές.",
|
"alert.no_feed": "Δεν έχετε συνδρομές.",
|
||||||
"alert.no_feed_in_category": "Δεν υπάρχει συνδρομή για αυτήν την κατηγορία.",
|
"alert.no_feed_in_category": "Δεν υπάρχει συνδρομή για αυτήν την κατηγορία.",
|
||||||
@ -303,14 +297,6 @@
|
|||||||
"error.password_min_length": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 6 χαρακτήρες.",
|
"error.password_min_length": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 6 χαρακτήρες.",
|
||||||
"error.settings_mandatory_fields": "Τα πεδία όνομα χρήστη, θέμα, Γλώσσα και ζώνη ώρας είναι υποχρεωτικά.",
|
"error.settings_mandatory_fields": "Τα πεδία όνομα χρήστη, θέμα, Γλώσσα και ζώνη ώρας είναι υποχρεωτικά.",
|
||||||
"error.settings_reading_speed_is_positive": "Οι ταχύτητες ανάγνωσης πρέπει να είναι θετικοί ακέραιοι αριθμοί.",
|
"error.settings_reading_speed_is_positive": "Οι ταχύτητες ανάγνωσης πρέπει να είναι θετικοί ακέραιοι αριθμοί.",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.entries_per_page_invalid": "Ο αριθμός των καταχωρήσεων ανά σελίδα δεν είναι έγκυρος.",
|
"error.entries_per_page_invalid": "Ο αριθμός των καταχωρήσεων ανά σελίδα δεν είναι έγκυρος.",
|
||||||
"error.feed_mandatory_fields": "Η διεύθυνση URL και η κατηγορία είναι υποχρεωτικά.",
|
"error.feed_mandatory_fields": "Η διεύθυνση URL και η κατηγορία είναι υποχρεωτικά.",
|
||||||
"error.feed_already_exists": "Αυτή η ροή υπάρχει ήδη.",
|
"error.feed_already_exists": "Αυτή η ροή υπάρχει ήδη.",
|
||||||
@ -330,7 +316,6 @@
|
|||||||
"form.feed.label.title": "Τίτλος",
|
"form.feed.label.title": "Τίτλος",
|
||||||
"form.feed.label.site_url": "Διεύθυνση URL ιστότοπου",
|
"form.feed.label.site_url": "Διεύθυνση URL ιστότοπου",
|
||||||
"form.feed.label.feed_url": "Διεύθυνση URL ροής",
|
"form.feed.label.feed_url": "Διεύθυνση URL ροής",
|
||||||
"form.feed.label.description": "Περιγραφή",
|
|
||||||
"form.feed.label.category": "Κατηγορία",
|
"form.feed.label.category": "Κατηγορία",
|
||||||
"form.feed.label.crawler": "Λήψη αρχικού περιεχομένου",
|
"form.feed.label.crawler": "Λήψη αρχικού περιεχομένου",
|
||||||
"form.feed.label.feed_username": "Όνομα Χρήστη ροής",
|
"form.feed.label.feed_username": "Όνομα Χρήστη ροής",
|
||||||
@ -352,13 +337,6 @@
|
|||||||
"form.feed.fieldset.rules": "Rules",
|
"form.feed.fieldset.rules": "Rules",
|
||||||
"form.feed.fieldset.network_settings": "Network Settings",
|
"form.feed.fieldset.network_settings": "Network Settings",
|
||||||
"form.feed.fieldset.integration": "Third-Party Services",
|
"form.feed.fieldset.integration": "Third-Party Services",
|
||||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
|
||||||
"form.category.label.title": "Τίτλος",
|
"form.category.label.title": "Τίτλος",
|
||||||
"form.category.hide_globally": "Απόκρυψη καταχωρήσεων σε γενική λίστα μη αναγνωσμένων",
|
"form.category.hide_globally": "Απόκρυψη καταχωρήσεων σε γενική λίστα μη αναγνωσμένων",
|
||||||
"form.user.label.username": "Χρήστης",
|
"form.user.label.username": "Χρήστης",
|
||||||
@ -395,18 +373,11 @@
|
|||||||
"form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα",
|
"form.prefs.label.default_home_page": "Προεπιλεγμένη αρχική σελίδα",
|
||||||
"form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών",
|
"form.prefs.label.categories_sorting_order": "Ταξινόμηση κατηγοριών",
|
||||||
"form.prefs.label.mark_read_on_view": "Αυτόματη επισήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή",
|
"form.prefs.label.mark_read_on_view": "Αυτόματη επισήμανση καταχωρήσεων ως αναγνωσμένων κατά την προβολή",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
|
||||||
"form.import.label.file": "Αρχείο OPML",
|
"form.import.label.file": "Αρχείο OPML",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "Save entries to Betula",
|
|
||||||
"form.integration.betula_url": "Betula server URL",
|
|
||||||
"form.integration.betula_token": "Betula Token",
|
|
||||||
"form.integration.fever_activate": "Ενεργοποιήστε το Fever API",
|
"form.integration.fever_activate": "Ενεργοποιήστε το Fever API",
|
||||||
"form.integration.fever_username": "Όνομα Χρήστη Fever",
|
"form.integration.fever_username": "Όνομα Χρήστη Fever",
|
||||||
"form.integration.fever_password": "Κωδικός Πρόσβασης Fever",
|
"form.integration.fever_password": "Κωδικός Πρόσβασης Fever",
|
||||||
@ -478,10 +449,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Κωδικός πρόσβασης για τον χρήστη Matrix",
|
"form.integration.matrix_bot_password": "Κωδικός πρόσβασης για τον χρήστη Matrix",
|
||||||
"form.integration.matrix_bot_url": "URL διακομιστή Matrix",
|
"form.integration.matrix_bot_url": "URL διακομιστή Matrix",
|
||||||
"form.integration.matrix_bot_chat_id": "Αναγνωριστικό της αίθουσας Matrix",
|
"form.integration.matrix_bot_chat_id": "Αναγνωριστικό της αίθουσας Matrix",
|
||||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "Αποθήκευση άρθρων στο Readeck",
|
"form.integration.readeck_activate": "Αποθήκευση άρθρων στο Readeck",
|
||||||
"form.integration.readeck_endpoint": "Τελικό σημείο Readeck API",
|
"form.integration.readeck_endpoint": "Τελικό σημείο Readeck API",
|
||||||
"form.integration.readeck_api_key": "Κλειδί API Readeck",
|
"form.integration.readeck_api_key": "Κλειδί API Readeck",
|
||||||
@ -499,13 +466,6 @@
|
|||||||
"form.integration.webhook_secret": "Webhook Secret",
|
"form.integration.webhook_secret": "Webhook Secret",
|
||||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
|
||||||
"form.api_key.label.description": "Ετικέτα κλειδιού API",
|
"form.api_key.label.description": "Ετικέτα κλειδιού API",
|
||||||
"form.submit.loading": "Φόρτωση...",
|
"form.submit.loading": "Φόρτωση...",
|
||||||
"form.submit.saving": "Αποθήκευση...",
|
"form.submit.saving": "Αποθήκευση...",
|
||||||
@ -545,7 +505,7 @@
|
|||||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP client error: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
@ -564,16 +524,5 @@
|
|||||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "Ταχύτητα αναπαραγωγής του ήχου/βίντεο",
|
|
||||||
"error.settings_media_playback_rate_range": "Η ταχύτητα αναπαραγωγής είναι εκτός εύρους",
|
|
||||||
"enclosure_media_controls.seek" : "Seek:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Speed:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Faster",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Slower",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,6 @@
|
|||||||
"menu.mark_page_as_read": "Mark this page as read",
|
"menu.mark_page_as_read": "Mark this page as read",
|
||||||
"menu.mark_all_as_read": "Mark all as read",
|
"menu.mark_all_as_read": "Mark all as read",
|
||||||
"menu.show_all_entries": "Show all entries",
|
"menu.show_all_entries": "Show all entries",
|
||||||
"menu.show_only_starred_entries": "Show only starred entries",
|
|
||||||
"menu.show_only_unread_entries": "Show only unread entries",
|
"menu.show_only_unread_entries": "Show only unread entries",
|
||||||
"menu.refresh_feed": "Refresh",
|
"menu.refresh_feed": "Refresh",
|
||||||
"menu.refresh_all_feeds": "Refresh all feeds in the background",
|
"menu.refresh_all_feeds": "Refresh all feeds in the background",
|
||||||
@ -56,9 +55,7 @@
|
|||||||
"search.label": "Search",
|
"search.label": "Search",
|
||||||
"search.placeholder": "Search…",
|
"search.placeholder": "Search…",
|
||||||
"search.submit": "Search",
|
"search.submit": "Search",
|
||||||
"pagination.last": "Last",
|
|
||||||
"pagination.next": "Next",
|
"pagination.next": "Next",
|
||||||
"pagination.first": "First",
|
|
||||||
"pagination.previous": "Previous",
|
"pagination.previous": "Previous",
|
||||||
"entry.status.unread": "Unread",
|
"entry.status.unread": "Unread",
|
||||||
"entry.status.read": "Read",
|
"entry.status.read": "Read",
|
||||||
@ -179,8 +176,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_previous_item": "Go to previous item",
|
"page.keyboard_shortcuts.go_to_previous_item": "Go to previous item",
|
||||||
"page.keyboard_shortcuts.go_to_next_item": "Go to next item",
|
"page.keyboard_shortcuts.go_to_next_item": "Go to next item",
|
||||||
"page.keyboard_shortcuts.go_to_feed": "Go to feed",
|
"page.keyboard_shortcuts.go_to_feed": "Go to feed",
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "Go to top item",
|
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "Go to bottom item",
|
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "Go to previous page",
|
"page.keyboard_shortcuts.go_to_previous_page": "Go to previous page",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "Go to next page",
|
"page.keyboard_shortcuts.go_to_next_page": "Go to next page",
|
||||||
"page.keyboard_shortcuts.open_item": "Open selected item",
|
"page.keyboard_shortcuts.open_item": "Open selected item",
|
||||||
@ -211,8 +206,8 @@
|
|||||||
"page.settings.title": "Settings",
|
"page.settings.title": "Settings",
|
||||||
"page.settings.link_google_account": "Link my Google account",
|
"page.settings.link_google_account": "Link my Google account",
|
||||||
"page.settings.unlink_google_account": "Unlink my Google account",
|
"page.settings.unlink_google_account": "Unlink my Google account",
|
||||||
"page.settings.link_oidc_account": "Link my %s account",
|
"page.settings.link_oidc_account": "Link my OpenID Connect account",
|
||||||
"page.settings.unlink_oidc_account": "Unlink my %s account",
|
"page.settings.unlink_oidc_account": "Unlink my OpenID Connect account",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Actions",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||||
@ -220,13 +215,13 @@
|
|||||||
"page.settings.webauthn.last_seen_on": "Last Used",
|
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||||
"page.settings.webauthn.register": "Register passkey",
|
"page.settings.webauthn.register": "Register passkey",
|
||||||
"page.settings.webauthn.register.error": "Unable to register passkey",
|
"page.settings.webauthn.register.error": "Unable to register passkey",
|
||||||
"page.settings.webauthn.delete": [
|
"page.settings.webauthn.delete" : [
|
||||||
"Remove %d passkey",
|
"Remove %d passkey",
|
||||||
"Remove %d passkeys"
|
"Remove %d passkeys"
|
||||||
],
|
],
|
||||||
"page.login.title": "Sign In",
|
"page.login.title": "Sign In",
|
||||||
"page.login.google_signin": "Sign in with Google",
|
"page.login.google_signin": "Sign in with Google",
|
||||||
"page.login.oidc_signin": "Sign in with %s",
|
"page.login.oidc_signin": "Sign in with OpenID Connect",
|
||||||
"page.login.webauthn_login": "Login with passkey",
|
"page.login.webauthn_login": "Login with passkey",
|
||||||
"page.login.webauthn_login.error": "Unable to login with passkey",
|
"page.login.webauthn_login.error": "Unable to login with passkey",
|
||||||
"page.integrations.title": "Integrations",
|
"page.integrations.title": "Integrations",
|
||||||
@ -261,7 +256,6 @@
|
|||||||
"alert.no_bookmark": "There are no starred entries.",
|
"alert.no_bookmark": "There are no starred entries.",
|
||||||
"alert.no_category": "There is no category.",
|
"alert.no_category": "There is no category.",
|
||||||
"alert.no_category_entry": "There are no entries in this category.",
|
"alert.no_category_entry": "There are no entries in this category.",
|
||||||
"alert.no_tag_entry": "There are no entries matching this tag.",
|
|
||||||
"alert.no_feed_entry": "There are no entries for this feed.",
|
"alert.no_feed_entry": "There are no entries for this feed.",
|
||||||
"alert.no_feed": "You don’t have any feeds.",
|
"alert.no_feed": "You don’t have any feeds.",
|
||||||
"alert.no_feed_in_category": "There is no feed for this category.",
|
"alert.no_feed_in_category": "There is no feed for this category.",
|
||||||
@ -303,14 +297,6 @@
|
|||||||
"error.password_min_length": "The password must have at least 6 characters.",
|
"error.password_min_length": "The password must have at least 6 characters.",
|
||||||
"error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.",
|
"error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.",
|
||||||
"error.settings_reading_speed_is_positive": "The reading speeds must be positive integers.",
|
"error.settings_reading_speed_is_positive": "The reading speeds must be positive integers.",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.entries_per_page_invalid": "The number of entries per page is not valid.",
|
"error.entries_per_page_invalid": "The number of entries per page is not valid.",
|
||||||
"error.feed_mandatory_fields": "The URL and the category are mandatory.",
|
"error.feed_mandatory_fields": "The URL and the category are mandatory.",
|
||||||
"error.feed_already_exists": "This feed already exists.",
|
"error.feed_already_exists": "This feed already exists.",
|
||||||
@ -328,7 +314,6 @@
|
|||||||
"form.feed.label.title": "Title",
|
"form.feed.label.title": "Title",
|
||||||
"form.feed.label.site_url": "Site URL",
|
"form.feed.label.site_url": "Site URL",
|
||||||
"form.feed.label.feed_url": "Feed URL",
|
"form.feed.label.feed_url": "Feed URL",
|
||||||
"form.feed.label.description": "Description",
|
|
||||||
"form.feed.label.category": "Category",
|
"form.feed.label.category": "Category",
|
||||||
"form.feed.label.crawler": "Fetch original content",
|
"form.feed.label.crawler": "Fetch original content",
|
||||||
"form.feed.label.feed_username": "Feed Username",
|
"form.feed.label.feed_username": "Feed Username",
|
||||||
@ -348,13 +333,6 @@
|
|||||||
"form.feed.label.disabled": "Do not refresh this feed",
|
"form.feed.label.disabled": "Do not refresh this feed",
|
||||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||||
"form.feed.label.hide_globally": "Hide entries in global unread list",
|
"form.feed.label.hide_globally": "Hide entries in global unread list",
|
||||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
|
||||||
"form.feed.fieldset.general": "General",
|
"form.feed.fieldset.general": "General",
|
||||||
"form.feed.fieldset.rules": "Rules",
|
"form.feed.fieldset.rules": "Rules",
|
||||||
"form.feed.fieldset.network_settings": "Network Settings",
|
"form.feed.fieldset.network_settings": "Network Settings",
|
||||||
@ -395,18 +373,11 @@
|
|||||||
"form.prefs.label.default_home_page": "Default home page",
|
"form.prefs.label.default_home_page": "Default home page",
|
||||||
"form.prefs.label.categories_sorting_order": "Categories sorting",
|
"form.prefs.label.categories_sorting_order": "Categories sorting",
|
||||||
"form.prefs.label.mark_read_on_view": "Automatically mark entries as read when viewed",
|
"form.prefs.label.mark_read_on_view": "Automatically mark entries as read when viewed",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
|
||||||
"form.import.label.file": "OPML file",
|
"form.import.label.file": "OPML file",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "Save entries to Betula",
|
|
||||||
"form.integration.betula_url": "Betula server URL",
|
|
||||||
"form.integration.betula_token": "Betula Token",
|
|
||||||
"form.integration.fever_activate": "Activate Fever API",
|
"form.integration.fever_activate": "Activate Fever API",
|
||||||
"form.integration.fever_username": "Fever Username",
|
"form.integration.fever_username": "Fever Username",
|
||||||
"form.integration.fever_password": "Fever Password",
|
"form.integration.fever_password": "Fever Password",
|
||||||
@ -478,10 +449,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Password for Matrix user",
|
"form.integration.matrix_bot_password": "Password for Matrix user",
|
||||||
"form.integration.matrix_bot_url": "Matrix server URL",
|
"form.integration.matrix_bot_url": "Matrix server URL",
|
||||||
"form.integration.matrix_bot_chat_id": "ID of Matrix Room",
|
"form.integration.matrix_bot_chat_id": "ID of Matrix Room",
|
||||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "Save entries to readeck",
|
"form.integration.readeck_activate": "Save entries to readeck",
|
||||||
"form.integration.readeck_endpoint": "Readeck API Endpoint",
|
"form.integration.readeck_endpoint": "Readeck API Endpoint",
|
||||||
"form.integration.readeck_api_key": "Readeck API key",
|
"form.integration.readeck_api_key": "Readeck API key",
|
||||||
@ -499,13 +466,6 @@
|
|||||||
"form.integration.webhook_secret": "Webhook Secret",
|
"form.integration.webhook_secret": "Webhook Secret",
|
||||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
|
||||||
"form.api_key.label.description": "API Key Label",
|
"form.api_key.label.description": "API Key Label",
|
||||||
"form.submit.loading": "Loading…",
|
"form.submit.loading": "Loading…",
|
||||||
"form.submit.saving": "Saving…",
|
"form.submit.saving": "Saving…",
|
||||||
@ -545,7 +505,7 @@
|
|||||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP client error: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
@ -564,16 +524,5 @@
|
|||||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "Playback speed of the audio/video",
|
|
||||||
"error.settings_media_playback_rate_range": "Playback speed is out of range",
|
|
||||||
"enclosure_media_controls.seek" : "Seek:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Speed:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Faster",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Slower",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"skip_to_content": "Saltar al contenido",
|
"skip_to_content": "Skip to content",
|
||||||
"confirm.question": "¿Estás seguro?",
|
"confirm.question": "¿Estás seguro?",
|
||||||
"confirm.question.refresh": "¿Quieres forzar la actualización?",
|
"confirm.question.refresh": "¿Quieres forzar la actualización?",
|
||||||
"confirm.yes": "sí",
|
"confirm.yes": "sí",
|
||||||
@ -9,8 +9,8 @@
|
|||||||
"action.save": "Guardar",
|
"action.save": "Guardar",
|
||||||
"action.or": "o",
|
"action.or": "o",
|
||||||
"action.cancel": "Cancelar",
|
"action.cancel": "Cancelar",
|
||||||
"action.remove": "Eliminar",
|
"action.remove": "Quitar",
|
||||||
"action.remove_feed": "Eliminar esta fuente",
|
"action.remove_feed": "Quitar esta fuente",
|
||||||
"action.update": "Actualizar",
|
"action.update": "Actualizar",
|
||||||
"action.edit": "Editar",
|
"action.edit": "Editar",
|
||||||
"action.download": "Descargar",
|
"action.download": "Descargar",
|
||||||
@ -19,8 +19,8 @@
|
|||||||
"action.home_screen": "Añadir a la pantalla principal",
|
"action.home_screen": "Añadir a la pantalla principal",
|
||||||
"tooltip.keyboard_shortcuts": "Atajo de teclado: %s",
|
"tooltip.keyboard_shortcuts": "Atajo de teclado: %s",
|
||||||
"tooltip.logged_user": "Registrado como %s",
|
"tooltip.logged_user": "Registrado como %s",
|
||||||
"menu.title": "Menú",
|
"menu.title": "Menu",
|
||||||
"menu.home_page": "Página de inicio",
|
"menu.home_page": "Home page",
|
||||||
"menu.unread": "No leídos",
|
"menu.unread": "No leídos",
|
||||||
"menu.starred": "Marcadores",
|
"menu.starred": "Marcadores",
|
||||||
"menu.history": "Historial",
|
"menu.history": "Historial",
|
||||||
@ -41,9 +41,8 @@
|
|||||||
"menu.mark_all_as_read": "Marcar todos como leídos",
|
"menu.mark_all_as_read": "Marcar todos como leídos",
|
||||||
"menu.show_all_entries": "Mostrar todos los artículos",
|
"menu.show_all_entries": "Mostrar todos los artículos",
|
||||||
"menu.show_only_unread_entries": "Mostrar solo los artículos no leídos",
|
"menu.show_only_unread_entries": "Mostrar solo los artículos no leídos",
|
||||||
"menu.show_only_starred_entries": "Mostrar solo los artículos marcados con una estrella",
|
|
||||||
"menu.refresh_feed": "Refrescar",
|
"menu.refresh_feed": "Refrescar",
|
||||||
"menu.refresh_all_feeds": "Refrescar todas las fuentes en segundo plano",
|
"menu.refresh_all_feeds": "Refrescar todas las fuentes en el fondo",
|
||||||
"menu.edit_feed": "Editar",
|
"menu.edit_feed": "Editar",
|
||||||
"menu.edit_category": "Editar",
|
"menu.edit_category": "Editar",
|
||||||
"menu.add_feed": "Agregar fuente",
|
"menu.add_feed": "Agregar fuente",
|
||||||
@ -55,10 +54,8 @@
|
|||||||
"menu.shared_entries": "Artículos compartidos",
|
"menu.shared_entries": "Artículos compartidos",
|
||||||
"search.label": "Buscar",
|
"search.label": "Buscar",
|
||||||
"search.placeholder": "Búsqueda...",
|
"search.placeholder": "Búsqueda...",
|
||||||
"search.submit": "Buscar",
|
"search.submit": "Search",
|
||||||
"pagination.last": "Último",
|
|
||||||
"pagination.next": "Siguiente",
|
"pagination.next": "Siguiente",
|
||||||
"pagination.first": "Primero",
|
|
||||||
"pagination.previous": "Anterior",
|
"pagination.previous": "Anterior",
|
||||||
"entry.status.unread": "No leído",
|
"entry.status.unread": "No leído",
|
||||||
"entry.status.read": "Leído",
|
"entry.status.read": "Leído",
|
||||||
@ -93,22 +90,22 @@
|
|||||||
"entry.tags.label": "Etiquetas:",
|
"entry.tags.label": "Etiquetas:",
|
||||||
"page.shared_entries.title": "Artículos compartidos",
|
"page.shared_entries.title": "Artículos compartidos",
|
||||||
"page.shared_entries_count": [
|
"page.shared_entries_count": [
|
||||||
"%d artículo compartido",
|
"%d shared entry",
|
||||||
"%d artículos compartidos"
|
"%d shared entries"
|
||||||
],
|
],
|
||||||
"page.unread.title": "No leídos",
|
"page.unread.title": "No leídos",
|
||||||
"page.unread_entry_count": [
|
"page.unread_entry_count": [
|
||||||
"%d artículo no leído",
|
"%d unread entry",
|
||||||
"%d artículos no leídos"
|
"%d unread entries"
|
||||||
],
|
],
|
||||||
"page.total_entry_count": [
|
"page.total_entry_count": [
|
||||||
"%d artículo en total",
|
"%d entry in total",
|
||||||
"%d artículos en total"
|
"%d entries in total"
|
||||||
],
|
],
|
||||||
"page.starred.title": "Marcadores",
|
"page.starred.title": "Marcadores",
|
||||||
"page.starred_entry_count": [
|
"page.starred_entry_count": [
|
||||||
"%d artículo marcado",
|
"%d starred entry",
|
||||||
"%d artículos marcados"
|
"%d starred entries"
|
||||||
],
|
],
|
||||||
"page.categories.title": "Categorías",
|
"page.categories.title": "Categorías",
|
||||||
"page.categories.no_feed": "Sin fuente.",
|
"page.categories.no_feed": "Sin fuente.",
|
||||||
@ -119,17 +116,17 @@
|
|||||||
"Hay %d fuentes."
|
"Hay %d fuentes."
|
||||||
],
|
],
|
||||||
"page.categories_count": [
|
"page.categories_count": [
|
||||||
"%d categoría",
|
"%d category",
|
||||||
"%d categorías"
|
"%d categories"
|
||||||
],
|
],
|
||||||
"page.new_category.title": "Nueva categoría",
|
"page.new_category.title": "Nueva categoría",
|
||||||
"page.new_user.title": "Nuevo usuario",
|
"page.new_user.title": "Nuevo usuario",
|
||||||
"page.edit_category.title": "Editar categoría: %s",
|
"page.edit_category.title": "Editar categoría: %s",
|
||||||
"page.edit_user.title": "Editar usuario: %s",
|
"page.edit_user.title": "Editar usuario: %s",
|
||||||
"page.feeds.title": "Fuentes",
|
"page.feeds.title": "Fuentes",
|
||||||
"page.category_label": "Categoría: %s",
|
"page.category_label": "Category: %s",
|
||||||
"page.feeds.last_check": "Última verificación:",
|
"page.feeds.last_check": "Última verificación:",
|
||||||
"page.feeds.next_check": "Próxima verificación:",
|
"page.feeds.next_check": "Next check:",
|
||||||
"page.feeds.read_counter": "Número de artículos leídos",
|
"page.feeds.read_counter": "Número de artículos leídos",
|
||||||
"page.feeds.error_count": [
|
"page.feeds.error_count": [
|
||||||
"%d error",
|
"%d error",
|
||||||
@ -137,15 +134,15 @@
|
|||||||
],
|
],
|
||||||
"page.history.title": "Historial",
|
"page.history.title": "Historial",
|
||||||
"page.read_entry_count": [
|
"page.read_entry_count": [
|
||||||
"%d artículo leído",
|
"%d read entry",
|
||||||
"%d artículos leídos"
|
"%d read entries"
|
||||||
],
|
],
|
||||||
"page.import.title": "Importar",
|
"page.import.title": "Importar",
|
||||||
"page.search.title": "Resultados de la búsqueda",
|
"page.search.title": "Resultados de la búsqueda",
|
||||||
"page.about.title": "Acerca de",
|
"page.about.title": "Acerca de",
|
||||||
"page.about.credits": "Créditos",
|
"page.about.credits": "Créditos",
|
||||||
"page.about.version": "Versión:",
|
"page.about.version": "Versión:",
|
||||||
"page.about.build_date": "Fecha de compilación:",
|
"page.about.build_date": "Fecha de construcción:",
|
||||||
"page.about.author": "Autor:",
|
"page.about.author": "Autor:",
|
||||||
"page.about.license": "Licencia:",
|
"page.about.license": "Licencia:",
|
||||||
"page.about.global_config_options": "Opciones de configuración global",
|
"page.about.global_config_options": "Opciones de configuración global",
|
||||||
@ -181,8 +178,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "Ir a la fuente",
|
"page.keyboard_shortcuts.go_to_feed": "Ir a la fuente",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "Ir al página anterior",
|
"page.keyboard_shortcuts.go_to_previous_page": "Ir al página anterior",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "Ir al página siguiente",
|
"page.keyboard_shortcuts.go_to_next_page": "Ir al página siguiente",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "Ir al elemento inferior",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "Ir al elemento superior",
|
|
||||||
"page.keyboard_shortcuts.open_item": "Abrir el elemento seleccionado",
|
"page.keyboard_shortcuts.open_item": "Abrir el elemento seleccionado",
|
||||||
"page.keyboard_shortcuts.open_original": "Abrir el enlace original",
|
"page.keyboard_shortcuts.open_original": "Abrir el enlace original",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "Abrir enlace original en la pestaña actual",
|
"page.keyboard_shortcuts.open_original_same_window": "Abrir enlace original en la pestaña actual",
|
||||||
@ -190,7 +185,7 @@
|
|||||||
"page.keyboard_shortcuts.open_comments_same_window": "Abrir enlace de comentarios en la pestaña actual",
|
"page.keyboard_shortcuts.open_comments_same_window": "Abrir enlace de comentarios en la pestaña actual",
|
||||||
"page.keyboard_shortcuts.toggle_read_status_next": "Marcar como leído o no leído, enfoque siguiente",
|
"page.keyboard_shortcuts.toggle_read_status_next": "Marcar como leído o no leído, enfoque siguiente",
|
||||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Marcar como leído o no leído, foco anterior",
|
"page.keyboard_shortcuts.toggle_read_status_prev": "Marcar como leído o no leído, foco anterior",
|
||||||
"page.keyboard_shortcuts.refresh_all_feeds": "Refrescar todas las fuentes en segundo plano",
|
"page.keyboard_shortcuts.refresh_all_feeds": "Refrescar todas las fuentes en el fondo",
|
||||||
"page.keyboard_shortcuts.mark_page_as_read": "Marcar página actual como leída",
|
"page.keyboard_shortcuts.mark_page_as_read": "Marcar página actual como leída",
|
||||||
"page.keyboard_shortcuts.download_content": "Descargar el contento original",
|
"page.keyboard_shortcuts.download_content": "Descargar el contento original",
|
||||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Agregar o quitar marcador",
|
"page.keyboard_shortcuts.toggle_bookmark_status": "Agregar o quitar marcador",
|
||||||
@ -211,24 +206,24 @@
|
|||||||
"page.settings.title": "Ajustes",
|
"page.settings.title": "Ajustes",
|
||||||
"page.settings.link_google_account": "Vincular mi cuenta de Google",
|
"page.settings.link_google_account": "Vincular mi cuenta de Google",
|
||||||
"page.settings.unlink_google_account": "Desvincular mi cuenta de Google",
|
"page.settings.unlink_google_account": "Desvincular mi cuenta de Google",
|
||||||
"page.settings.link_oidc_account": "Vincular mi cuenta de %s",
|
"page.settings.link_oidc_account": "Vincular mi cuenta de OpenID Connect",
|
||||||
"page.settings.unlink_oidc_account": "Desvincular mi cuenta de %s",
|
"page.settings.unlink_oidc_account": "Desvincular mi cuenta de OpenID Connect",
|
||||||
"page.settings.webauthn.passkeys": "Claves de acceso",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Accioness",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Nombre de clave de acceso",
|
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||||
"page.settings.webauthn.added_on": "Añadido",
|
"page.settings.webauthn.added_on": "Added On",
|
||||||
"page.settings.webauthn.last_seen_on": "Usado por última vez",
|
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||||
"page.settings.webauthn.register": "Registrar clave de acceso",
|
"page.settings.webauthn.register": "Registrar clave de acceso",
|
||||||
"page.settings.webauthn.register.error": "No se puede registrar la clave de acceso",
|
"page.settings.webauthn.register.error": "No se puede registrar la clave de paso",
|
||||||
"page.settings.webauthn.delete": [
|
"page.settings.webauthn.delete": [
|
||||||
"Eliminar %d clave de acceso",
|
"Eliminar %d clave de paso",
|
||||||
"Eliminar %d claves de acceso"
|
"Eliminar %d claves de paso"
|
||||||
],
|
],
|
||||||
"page.login.title": "Iniciar sesión",
|
"page.login.title": "Iniciar sesión",
|
||||||
"page.login.google_signin": "Iniciar sesión con tu cuenta de Google",
|
"page.login.google_signin": "Iniciar sesión con tu cuenta de Google",
|
||||||
"page.login.oidc_signin": "Iniciar sesión con tu cuenta de %s",
|
"page.login.oidc_signin": "Iniciar sesión con tu cuenta de OpenID Connect",
|
||||||
"page.login.webauthn_login": "Iniciar sesión con clave de acceso",
|
"page.login.webauthn_login": "Iniciar sesión con clave de acceso",
|
||||||
"page.login.webauthn_login.error": "No se puede iniciar sesión con la clave de acceso",
|
"page.login.webauthn_login.error": "No se puede iniciar sesión con la clave de paso",
|
||||||
"page.integrations.title": "Integraciones",
|
"page.integrations.title": "Integraciones",
|
||||||
"page.integration.miniflux_api": "API de Miniflux",
|
"page.integration.miniflux_api": "API de Miniflux",
|
||||||
"page.integration.miniflux_api_endpoint": "Extremo de API",
|
"page.integration.miniflux_api_endpoint": "Extremo de API",
|
||||||
@ -256,12 +251,11 @@
|
|||||||
"page.offline.title": "Modo offline",
|
"page.offline.title": "Modo offline",
|
||||||
"page.offline.message": "Estas desconectado",
|
"page.offline.message": "Estas desconectado",
|
||||||
"page.offline.refresh_page": "Intenta actualizar la página",
|
"page.offline.refresh_page": "Intenta actualizar la página",
|
||||||
"page.webauthn_rename.title": "Renombrar clave de acceso",
|
"page.webauthn_rename.title": "Rename Passkey",
|
||||||
"alert.no_shared_entry": "No hay artículos compartidos.",
|
"alert.no_shared_entry": "No hay artículos compartidos.",
|
||||||
"alert.no_bookmark": "No hay marcador en este momento.",
|
"alert.no_bookmark": "No hay marcador en este momento.",
|
||||||
"alert.no_category": "No hay categoría.",
|
"alert.no_category": "No hay categoría.",
|
||||||
"alert.no_category_entry": "No hay artículos en esta categoría.",
|
"alert.no_category_entry": "No hay artículos en esta categoría.",
|
||||||
"alert.no_tag_entry": "No hay artículos con esta etiqueta.",
|
|
||||||
"alert.no_feed_entry": "No hay artículos para esta fuente.",
|
"alert.no_feed_entry": "No hay artículos para esta fuente.",
|
||||||
"alert.no_feed": "No tienes fuentes.",
|
"alert.no_feed": "No tienes fuentes.",
|
||||||
"alert.no_feed_in_category": "No hay fuentes para esta categoría.",
|
"alert.no_feed_in_category": "No hay fuentes para esta categoría.",
|
||||||
@ -296,14 +290,6 @@
|
|||||||
"error.password_min_length": "La contraseña debería tener al menos 6 caracteres.",
|
"error.password_min_length": "La contraseña debería tener al menos 6 caracteres.",
|
||||||
"error.settings_mandatory_fields": "Los campos de nombre de usuario, tema, idioma y zona horaria son obligatorios.",
|
"error.settings_mandatory_fields": "Los campos de nombre de usuario, tema, idioma y zona horaria son obligatorios.",
|
||||||
"error.settings_reading_speed_is_positive": "Las velocidades de lectura deben ser números enteros positivos.",
|
"error.settings_reading_speed_is_positive": "Las velocidades de lectura deben ser números enteros positivos.",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Regla de bloqueo no válida: a la regla #%d le falta un nombre de campo válido (Opciones: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Regla de bloqueo no válida: el patrón de la regla #%d debe estar separado por un '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Regla de bloqueo no válida: no se ha proporcionado el patrón de la regla #%d",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Regla de bloqueo no válida: el patrón de la regla #%d no es una expresión regular válida",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Regla de mantenimiento no válida: a la regla #%d le falta un nombre de campo válido (Opciones: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Regla de mantenimiento no válida: el patrón de la regla #%d debe estar separado por un '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Regla de conservación no válida: no se ha proporcionado la regla #%d patrón",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Regla de mantenimiento no válida: el patrón de la regla #%d no es una expresión regular válida",
|
|
||||||
"error.entries_per_page_invalid": "El número de artículos por página no es válido.",
|
"error.entries_per_page_invalid": "El número de artículos por página no es válido.",
|
||||||
"error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.",
|
"error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.",
|
||||||
"error.feed_already_exists": "Este feed ya existe.",
|
"error.feed_already_exists": "Este feed ya existe.",
|
||||||
@ -328,7 +314,6 @@
|
|||||||
"form.feed.label.title": "Título",
|
"form.feed.label.title": "Título",
|
||||||
"form.feed.label.site_url": "URL del sitio",
|
"form.feed.label.site_url": "URL del sitio",
|
||||||
"form.feed.label.feed_url": "URL de la fuente",
|
"form.feed.label.feed_url": "URL de la fuente",
|
||||||
"form.feed.label.description": "Descripción",
|
|
||||||
"form.feed.label.category": "Categoría",
|
"form.feed.label.category": "Categoría",
|
||||||
"form.feed.label.crawler": "Obtener rastreador original",
|
"form.feed.label.crawler": "Obtener rastreador original",
|
||||||
"form.feed.label.feed_username": "Nombre de usuario de la fuente",
|
"form.feed.label.feed_username": "Nombre de usuario de la fuente",
|
||||||
@ -337,28 +322,21 @@
|
|||||||
"form.feed.label.cookie": "Configurar las cookies",
|
"form.feed.label.cookie": "Configurar las cookies",
|
||||||
"form.feed.label.scraper_rules": "Reglas de extracción de información",
|
"form.feed.label.scraper_rules": "Reglas de extracción de información",
|
||||||
"form.feed.label.rewrite_rules": "Reglas de reescribir",
|
"form.feed.label.rewrite_rules": "Reglas de reescribir",
|
||||||
"form.feed.label.apprise_service_urls": "Lista separada por comas de las URL del servicio Apprise",
|
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||||
"form.feed.label.blocklist_rules": "Reglas de Filtrado (Bloquear)",
|
"form.feed.label.blocklist_rules": "Reglas de Filtrado (Bloquear)",
|
||||||
"form.feed.label.keeplist_rules": "Reglas de Filtrado (Permitir)",
|
"form.feed.label.keeplist_rules": "Reglas de Filtrado (Permitir)",
|
||||||
"form.feed.label.urlrewrite_rules": "Reglas de Filtrado (Reescritura)",
|
"form.feed.label.urlrewrite_rules": "Reglas de Filtrado (Reescritura)",
|
||||||
"form.feed.label.ignore_http_cache": "Ignorar caché HTTP",
|
"form.feed.label.ignore_http_cache": "Ignorar caché HTTP",
|
||||||
"form.feed.label.allow_self_signed_certificates": "Permitir certificados autofirmados o no válidos",
|
"form.feed.label.allow_self_signed_certificates": "Permitir certificados autofirmados o no válidos",
|
||||||
"form.feed.label.disable_http2": "Deshabilite HTTP/2 para evitar huellas digitales",
|
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||||
"form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
|
"form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
|
||||||
"form.feed.label.disabled": "No actualice este feed",
|
"form.feed.label.disabled": "No actualice este feed",
|
||||||
"form.feed.label.no_media_player": "Sin reproductor multimedia (audio/video)",
|
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||||
"form.feed.label.hide_globally": "Ocultar artículos en la lista global de no leídos",
|
"form.feed.label.hide_globally": "Ocultar artículos en la lista global de no leídos",
|
||||||
"form.feed.label.ntfy_activate": "Enviar entradas a ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Prioridad Ntfy",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Prioridad máxima a Ntfy",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Prioridad alta a Ntfy",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Prioridad predeterminada a Ntfy",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Prioridad baja a Ntfy",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Prioridad mínima a Ntfy",
|
|
||||||
"form.feed.fieldset.general": "General",
|
"form.feed.fieldset.general": "General",
|
||||||
"form.feed.fieldset.rules": "Reglas",
|
"form.feed.fieldset.rules": "Rules",
|
||||||
"form.feed.fieldset.network_settings": "Ajustes de red",
|
"form.feed.fieldset.network_settings": "Network Settings",
|
||||||
"form.feed.fieldset.integration": "Servicios de terceros",
|
"form.feed.fieldset.integration": "Third-Party Services",
|
||||||
"form.category.label.title": "Título",
|
"form.category.label.title": "Título",
|
||||||
"form.category.hide_globally": "Ocultar artículos en la lista global de no leídos",
|
"form.category.hide_globally": "Ocultar artículos en la lista global de no leídos",
|
||||||
"form.user.label.username": "Nombre de usuario",
|
"form.user.label.username": "Nombre de usuario",
|
||||||
@ -395,18 +373,11 @@
|
|||||||
"form.prefs.label.default_home_page": "Página de inicio por defecto",
|
"form.prefs.label.default_home_page": "Página de inicio por defecto",
|
||||||
"form.prefs.label.categories_sorting_order": "Clasificación por categorías",
|
"form.prefs.label.categories_sorting_order": "Clasificación por categorías",
|
||||||
"form.prefs.label.mark_read_on_view": "Marcar automáticamente las entradas como leídas cuando se vean",
|
"form.prefs.label.mark_read_on_view": "Marcar automáticamente las entradas como leídas cuando se vean",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Marcar las entradas como leídas cuando se vean. Para audio/video, marcar como leído al 90%% de finalización",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Marcar como leído solo cuando la reproducción de audio/video alcance el 90%% de finalización",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.label.mark_read_manually": "Marcar entradas como leídas manualmente",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
"form.prefs.fieldset.application_settings": "Ajustes de la aplicación",
|
|
||||||
"form.prefs.fieldset.authentication_settings": "Ajustes de la autentificación",
|
|
||||||
"form.prefs.fieldset.reader_settings": "Ajustes del lector",
|
|
||||||
"form.prefs.fieldset.global_feed_settings": "Ajustes globales del feed",
|
|
||||||
"form.import.label.file": "Archivo OPML",
|
"form.import.label.file": "Archivo OPML",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "Guardar artículos en Betula",
|
|
||||||
"form.integration.betula_url": "URL del servidor Betula",
|
|
||||||
"form.integration.betula_token": "Token de Betula",
|
|
||||||
"form.integration.fever_activate": "Activar API de Fever",
|
"form.integration.fever_activate": "Activar API de Fever",
|
||||||
"form.integration.fever_username": "Nombre de usuario de Fever",
|
"form.integration.fever_username": "Nombre de usuario de Fever",
|
||||||
"form.integration.fever_password": "Contraseña de Fever",
|
"form.integration.fever_password": "Contraseña de Fever",
|
||||||
@ -433,12 +404,12 @@
|
|||||||
"form.integration.wallabag_client_secret": "Secreto de cliente de Wallabag",
|
"form.integration.wallabag_client_secret": "Secreto de cliente de Wallabag",
|
||||||
"form.integration.wallabag_username": "Nombre de usuario de Wallabag",
|
"form.integration.wallabag_username": "Nombre de usuario de Wallabag",
|
||||||
"form.integration.wallabag_password": "Contraseña de Wallabag",
|
"form.integration.wallabag_password": "Contraseña de Wallabag",
|
||||||
"form.integration.notion_activate": "Guardar entradas en Notion",
|
"form.integration.notion_activate": "Save entries to Notion",
|
||||||
"form.integration.notion_page_id": "ID de página de Notion",
|
"form.integration.notion_page_id": "Notion Page ID",
|
||||||
"form.integration.notion_token": "Token secreto de Notion",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Enviar artículos a Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "URL de la API de Apprise",
|
"form.integration.apprise_url": "Apprise API URL",
|
||||||
"form.integration.apprise_services_url": "Lista separada por comas de las URL del servicio Apprise",
|
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||||
"form.integration.nunux_keeper_activate": "Enviar artículos a Nunux Keeper",
|
"form.integration.nunux_keeper_activate": "Enviar artículos a Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Acceso API de Nunux Keeper",
|
"form.integration.nunux_keeper_endpoint": "Acceso API de Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_api_key": "Clave de API de Nunux Keeper",
|
"form.integration.nunux_keeper_api_key": "Clave de API de Nunux Keeper",
|
||||||
@ -449,26 +420,26 @@
|
|||||||
"form.integration.espial_endpoint": "Acceso API de Espial",
|
"form.integration.espial_endpoint": "Acceso API de Espial",
|
||||||
"form.integration.espial_api_key": "Clave de API de Espial",
|
"form.integration.espial_api_key": "Clave de API de Espial",
|
||||||
"form.integration.espial_tags": "Etiquetas de Espial",
|
"form.integration.espial_tags": "Etiquetas de Espial",
|
||||||
"form.integration.readwise_activate": "Guardar artículos en Readwise Reader",
|
"form.integration.readwise_activate": "Save entries to Readwise Reader",
|
||||||
"form.integration.readwise_api_key": "Token de acceso a Readwise Reader",
|
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||||
"form.integration.readwise_api_key_link": "Obtener tu token de acceso a Readwise",
|
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||||
"form.integration.telegram_bot_activate": "Envíe nuevos artículos al chat de Telegram",
|
"form.integration.telegram_bot_activate": "Envíe nuevos artículos al chat de Telegram",
|
||||||
"form.integration.telegram_bot_token": "Token de bot",
|
"form.integration.telegram_bot_token": "Token de bot",
|
||||||
"form.integration.telegram_chat_id": "ID de chat",
|
"form.integration.telegram_chat_id": "ID de chat",
|
||||||
"form.integration.telegram_topic_id": "Topic ID",
|
"form.integration.telegram_topic_id": "Topic ID",
|
||||||
"form.integration.telegram_bot_disable_web_page_preview": "Deshabilitar la vista previa de la página web",
|
"form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
|
||||||
"form.integration.telegram_bot_disable_notification": "Deshabilitar notificación",
|
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||||
"form.integration.telegram_bot_disable_buttons": "Deshabilitar botones",
|
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||||
"form.integration.linkace_activate": "Guardar artículos en LinkAce",
|
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||||
"form.integration.linkace_api_key": "Clave API de LinkAce",
|
"form.integration.linkace_api_key": "LinkAce API key",
|
||||||
"form.integration.linkace_tags": "Etiquetas de LinkAce",
|
"form.integration.linkace_tags": "LinkAce Tags",
|
||||||
"form.integration.linkace_is_private": "Marcar enlace como privado",
|
"form.integration.linkace_is_private": "Mark link as private",
|
||||||
"form.integration.linkace_check_disabled": "Deshabilitar la comprobación de enlace",
|
"form.integration.linkace_check_disabled": "Disable link check",
|
||||||
"form.integration.linkding_activate": "Enviar artículos a Linkding",
|
"form.integration.linkding_activate": "Enviar artículos a Linkding",
|
||||||
"form.integration.linkding_endpoint": "Acceso API de Linkding",
|
"form.integration.linkding_endpoint": "Acceso API de Linkding",
|
||||||
"form.integration.linkding_api_key": "Clave de API de Linkding",
|
"form.integration.linkding_api_key": "Clave de API de Linkding",
|
||||||
"form.integration.linkding_tags": "Etiquetas de Linkding",
|
"form.integration.linkding_tags": "Linkding Tags",
|
||||||
"form.integration.linkding_bookmark": "Marcar marcador como no leído",
|
"form.integration.linkding_bookmark": "Marcar marcador como no leído",
|
||||||
"form.integration.linkwarden_activate": "Enviar artículos a Linkwarden",
|
"form.integration.linkwarden_activate": "Enviar artículos a Linkwarden",
|
||||||
"form.integration.linkwarden_endpoint": "Acceso API de Linkwarden",
|
"form.integration.linkwarden_endpoint": "Acceso API de Linkwarden",
|
||||||
@ -478,34 +449,23 @@
|
|||||||
"form.integration.matrix_bot_password": "Contraseña para el usuario de Matrix",
|
"form.integration.matrix_bot_password": "Contraseña para el usuario de Matrix",
|
||||||
"form.integration.matrix_bot_url": "URL del servidor de Matrix",
|
"form.integration.matrix_bot_url": "URL del servidor de Matrix",
|
||||||
"form.integration.matrix_bot_chat_id": "ID de la sala de Matrix",
|
"form.integration.matrix_bot_chat_id": "ID de la sala de Matrix",
|
||||||
"form.integration.raindrop_activate": "Guardar artículos en Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Colección ID",
|
|
||||||
"form.integration.raindrop_tags": "Etiquetas (separadas por comas)",
|
|
||||||
"form.integration.readeck_activate": "Enviar artículos a Readeck",
|
"form.integration.readeck_activate": "Enviar artículos a Readeck",
|
||||||
"form.integration.readeck_endpoint": "Acceso API de Readeck",
|
"form.integration.readeck_endpoint": "Acceso API de Readeck",
|
||||||
"form.integration.readeck_api_key": "Clave de API de Readeck",
|
"form.integration.readeck_api_key": "Clave de API de Readeck",
|
||||||
"form.integration.readeck_labels": "Etiquetas de Readeck",
|
"form.integration.readeck_labels": "Readeck Labels",
|
||||||
"form.integration.readeck_only_url": "Enviar solo URL (en lugar de contenido completo)",
|
"form.integration.readeck_only_url": "Enviar solo URL (en lugar de contenido completo)",
|
||||||
"form.integration.shiori_activate": "Guardar artículos a Shiori",
|
"form.integration.shiori_activate": "Guardar artículos a Shiori",
|
||||||
"form.integration.shiori_endpoint": "Extremo de API de Shiori",
|
"form.integration.shiori_endpoint": "Extremo de API de Shiori",
|
||||||
"form.integration.shiori_username": "Nombre de usuario de Shiori",
|
"form.integration.shiori_username": "Nombre de usuario de Shiori",
|
||||||
"form.integration.shiori_password": "Contraseña de Shiori",
|
"form.integration.shiori_password": "Contraseña de Shiori",
|
||||||
"form.integration.shaarli_activate": "Guardar artículos en Shaarli",
|
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||||
"form.integration.shaarli_endpoint": "URL de Shaarli",
|
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||||
"form.integration.shaarli_api_secret": "Secreto API de Shaarli",
|
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||||
"form.integration.webhook_activate": "Habilitar Webhook",
|
"form.integration.webhook_activate": "Enable Webhook",
|
||||||
"form.integration.webhook_url": "URL de Webhook",
|
"form.integration.webhook_url": "Webhook URL",
|
||||||
"form.integration.webhook_secret": "Secreto de Webhook",
|
"form.integration.webhook_secret": "Webhook Secret",
|
||||||
"form.integration.rssbridge_activate": "Vericar RSS-Bridge al agregar suscripciones",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "URL del servidro RSS-Bridge",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Enviar artículos a ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Tema Ntfy",
|
|
||||||
"form.integration.ntfy_url": "URL de Ntfy (opcional, la predeterminada es ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Token de API de Ntfy (opcional)",
|
|
||||||
"form.integration.ntfy_username": "Nombre de usuario de Ntfy (opcional)",
|
|
||||||
"form.integration.ntfy_password": "Contraseña de Ntfy (opcional)",
|
|
||||||
"form.integration.ntfy_icon_url": "URL del icono de Ntfy (opcional)",
|
|
||||||
"form.api_key.label.description": "Etiqueta de clave API",
|
"form.api_key.label.description": "Etiqueta de clave API",
|
||||||
"form.submit.loading": "Cargando...",
|
"form.submit.loading": "Cargando...",
|
||||||
"form.submit.saving": "Guardando...",
|
"form.submit.saving": "Guardando...",
|
||||||
@ -537,43 +497,32 @@
|
|||||||
"hace %d años"
|
"hace %d años"
|
||||||
],
|
],
|
||||||
"alert.too_many_feeds_refresh": [
|
"alert.too_many_feeds_refresh": [
|
||||||
"Has activado demasiadas actualizaciones del feed. Espere %d minuto antes de volver a intentarlo.",
|
"You have triggered too many feed refreshes. Please wait %d minute before trying again.",
|
||||||
"Has activado demasiadas actualizaciones del feed. Espere %d minutos antes de volver a intentarlo."
|
"You have triggered too many feed refreshes. Please wait %d minutes before trying again."
|
||||||
],
|
],
|
||||||
"alert.background_feed_refresh": "Todos los feeds se actualizan en segundo plano. Puede continuar usando Miniflux mientras se ejecuta este proceso.",
|
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||||
"error.http_response_too_large": "La respuesta HTTP es demasiado grande. Puede aumentar el límite de tamaño de respuesta HTTP en la configuración global (requiere reiniciar el servidor).",
|
"error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",
|
||||||
"error.http_body_read": "Imposible leer el cuerpo HTTP: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "El cuerpo de la respuesta HTTP está vacío.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "La respuesta HTTP está vacía. ¿Quizás este sitio web tiene un mecanismo de protección contra bots?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "Error de TLS: %q. Puede desactivar la verificación TLS en la configuración del feed si lo desea.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux no puede acceder a este sitio web debido a un error de red: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "Este sitio web es demasiado lento y se agotó el tiempo de espera de la solicitud: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "Error cliente HTTP: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
"error.http_not_authorized": "El acceso a este sitio web no está autorizado. Podría ser un nombre de usuario o contraseña incorrectos.",
|
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||||
"error.http_too_many_requests": "Miniflux generó demasiadas solicitudes a este sitio web. Por favor, inténtalo de nuevo más tarde o cambia la configuración de la aplicación.",
|
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||||
"error.http_forbidden": "El acceso a este sitio web está prohibido. ¿Quizás este sitio web tiene un mecanismo de protección contra bots?",
|
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||||
"error.http_resource_not_found": "No se encuentra el recurso solicitado. Por favor, verifique la URL.",
|
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||||
"error.http_internal_server_error": "El sitio web no está disponible en estos momentos debido a un error del servidor. El problema no está en el lado de Miniflux. Por favor, inténtalo de nuevo más tarde.",
|
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.http_bad_gateway": "El sitio web no está disponible en este momento debido a un error en la puerta de enlace. El problema no está en el lado de Miniflux. Por favor, inténtalo de nuevo más tarde.",
|
"error.http_bad_gateway": "The website is not available at the moment due to a bad gateway error. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.http_service_unavailable": "El sitio web no está disponible en estos momentos debido a un error interno del servidor. El problema no está en el lado de Miniflux. Por favor, inténtalo de nuevo más tarde.",
|
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.http_gateway_timeout": "El sitio web no está disponible en este momento debido a un error de tiempo de espera de la puerta de enlace. El problema no está en el lado de Miniflux. Por favor, inténtalo de nuevo más tarde.",
|
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.http_unexpected_status_code": "El sitio web no está disponible en este momento debido a un código de estado HTTP inesperado: %d. El problema no está en el lado de Miniflux. Por favor, inténtalo de nuevo más tarde.",
|
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.database_error": "Error en la base de datos: %v.",
|
"error.database_error": "Database error: %v.",
|
||||||
"error.category_not_found": "Esta categoría no existe o no pertenece a este usuario.",
|
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||||
"error.duplicated_feed": "Este feed ya existe.",
|
"error.duplicated_feed": "This feed already exists.",
|
||||||
"error.unable_to_parse_feed": "No se puede analizar este feed: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "Este feed no existe o no pertenece a este usuario.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "No se puede detectar la fuente usando RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "No se puede detectar el formato del feed: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "Velocidad de reproducción del audio/vídeo",
|
|
||||||
"error.settings_media_playback_rate_range": "La velocidad de reproducción está fuera de rango",
|
|
||||||
"enclosure_media_controls.seek" : "Buscar:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Buscar %s segundos",
|
|
||||||
"enclosure_media_controls.speed" : "Velocidad:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Más rápido",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Más rápido a %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Despacio",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Más despacio a %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Restablecer",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Restablecer la velocidad a 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "Merkitse kaikki luetuksi",
|
"menu.mark_all_as_read": "Merkitse kaikki luetuksi",
|
||||||
"menu.show_all_entries": "Näytä kaikki artikkelit",
|
"menu.show_all_entries": "Näytä kaikki artikkelit",
|
||||||
"menu.show_only_unread_entries": "Näytä vain lukemattomat artikkelit",
|
"menu.show_only_unread_entries": "Näytä vain lukemattomat artikkelit",
|
||||||
"menu.show_only_starred_entries": "Show only starred entries",
|
|
||||||
"menu.refresh_feed": "Päivitä",
|
"menu.refresh_feed": "Päivitä",
|
||||||
"menu.refresh_all_feeds": "Päivitä kaikki syötteet taustalla",
|
"menu.refresh_all_feeds": "Päivitä kaikki syötteet taustalla",
|
||||||
"menu.edit_feed": "Muokkaa",
|
"menu.edit_feed": "Muokkaa",
|
||||||
@ -56,9 +55,7 @@
|
|||||||
"search.label": "Haku",
|
"search.label": "Haku",
|
||||||
"search.placeholder": "Hae...",
|
"search.placeholder": "Hae...",
|
||||||
"search.submit": "Search",
|
"search.submit": "Search",
|
||||||
"pagination.last": "Last",
|
|
||||||
"pagination.next": "Seuraava",
|
"pagination.next": "Seuraava",
|
||||||
"pagination.first": "First",
|
|
||||||
"pagination.previous": "Edellinen",
|
"pagination.previous": "Edellinen",
|
||||||
"entry.status.unread": "Lukematon",
|
"entry.status.unread": "Lukematon",
|
||||||
"entry.status.read": "Luettu",
|
"entry.status.read": "Luettu",
|
||||||
@ -181,8 +178,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "Siirry syötteeseen",
|
"page.keyboard_shortcuts.go_to_feed": "Siirry syötteeseen",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "Siirry edelliselle sivulle",
|
"page.keyboard_shortcuts.go_to_previous_page": "Siirry edelliselle sivulle",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "Siirry seuraavalle sivulle",
|
"page.keyboard_shortcuts.go_to_next_page": "Siirry seuraavalle sivulle",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "Siirry alimpaan kohtaan",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "Siirry alkuun",
|
|
||||||
"page.keyboard_shortcuts.open_item": "Avaa valittu kohde",
|
"page.keyboard_shortcuts.open_item": "Avaa valittu kohde",
|
||||||
"page.keyboard_shortcuts.open_original": "Avaa alkuperäinen linkki",
|
"page.keyboard_shortcuts.open_original": "Avaa alkuperäinen linkki",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "Avaa alkuperäinen linkki nykyisessä välilehdessä",
|
"page.keyboard_shortcuts.open_original_same_window": "Avaa alkuperäinen linkki nykyisessä välilehdessä",
|
||||||
@ -211,8 +206,8 @@
|
|||||||
"page.settings.title": "Asetukset",
|
"page.settings.title": "Asetukset",
|
||||||
"page.settings.link_google_account": "Linkitä Google-tilini",
|
"page.settings.link_google_account": "Linkitä Google-tilini",
|
||||||
"page.settings.unlink_google_account": "Poista Google-tilini linkitys",
|
"page.settings.unlink_google_account": "Poista Google-tilini linkitys",
|
||||||
"page.settings.link_oidc_account": "Linkitä %s -tilini",
|
"page.settings.link_oidc_account": "Linkitä OpenID Connect -tilini",
|
||||||
"page.settings.unlink_oidc_account": "Poista %s -tilini linkitys",
|
"page.settings.unlink_oidc_account": "Poista OpenID Connect -tilini linkitys",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Actions",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||||
@ -226,7 +221,7 @@
|
|||||||
],
|
],
|
||||||
"page.login.title": "Kirjaudu sisään",
|
"page.login.title": "Kirjaudu sisään",
|
||||||
"page.login.google_signin": "Kirjaudu sisään Googlella",
|
"page.login.google_signin": "Kirjaudu sisään Googlella",
|
||||||
"page.login.oidc_signin": "Kirjaudu sisään %silla",
|
"page.login.oidc_signin": "Kirjaudu sisään OpenID Connectilla",
|
||||||
"page.login.webauthn_login": "Kirjaudu sisään salasanalla",
|
"page.login.webauthn_login": "Kirjaudu sisään salasanalla",
|
||||||
"page.login.webauthn_login.error": "Ei voida kirjautua sisään salasanalla",
|
"page.login.webauthn_login.error": "Ei voida kirjautua sisään salasanalla",
|
||||||
"page.integrations.title": "Integraatiot",
|
"page.integrations.title": "Integraatiot",
|
||||||
@ -261,7 +256,6 @@
|
|||||||
"alert.no_bookmark": "Tällä hetkellä ei ole kirjanmerkkiä.",
|
"alert.no_bookmark": "Tällä hetkellä ei ole kirjanmerkkiä.",
|
||||||
"alert.no_category": "Ei ole kategoriaa.",
|
"alert.no_category": "Ei ole kategoriaa.",
|
||||||
"alert.no_category_entry": "Tässä kategoriassa ei ole artikkeleita.",
|
"alert.no_category_entry": "Tässä kategoriassa ei ole artikkeleita.",
|
||||||
"alert.no_tag_entry": "Tätä tunnistetta vastaavia merkintöjä ei ole.",
|
|
||||||
"alert.no_feed_entry": "Tässä syötteessä ei ole artikkeleita.",
|
"alert.no_feed_entry": "Tässä syötteessä ei ole artikkeleita.",
|
||||||
"alert.no_feed": "Sinulla ei ole tilauksia.",
|
"alert.no_feed": "Sinulla ei ole tilauksia.",
|
||||||
"alert.no_feed_in_category": "Tälle kategorialle ei ole tilausta.",
|
"alert.no_feed_in_category": "Tälle kategorialle ei ole tilausta.",
|
||||||
@ -303,14 +297,6 @@
|
|||||||
"error.password_min_length": "Salasanassa on oltava vähintään 6 merkkiä.",
|
"error.password_min_length": "Salasanassa on oltava vähintään 6 merkkiä.",
|
||||||
"error.settings_mandatory_fields": "Käyttäjätunnus, teema, kieli ja aikavyöhyke ovat pakollisia.",
|
"error.settings_mandatory_fields": "Käyttäjätunnus, teema, kieli ja aikavyöhyke ovat pakollisia.",
|
||||||
"error.settings_reading_speed_is_positive": "Lukunopeuksien on oltava positiivisia kokonaislukuja.",
|
"error.settings_reading_speed_is_positive": "Lukunopeuksien on oltava positiivisia kokonaislukuja.",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.entries_per_page_invalid": "Artikkelien määrä sivulla ei kelpaa.",
|
"error.entries_per_page_invalid": "Artikkelien määrä sivulla ei kelpaa.",
|
||||||
"error.feed_mandatory_fields": "URL-osoite ja kategoria ovat pakollisia.",
|
"error.feed_mandatory_fields": "URL-osoite ja kategoria ovat pakollisia.",
|
||||||
"error.feed_already_exists": "Tämä syöte on jo olemassa.",
|
"error.feed_already_exists": "Tämä syöte on jo olemassa.",
|
||||||
@ -330,7 +316,6 @@
|
|||||||
"form.feed.label.title": "Otsikko",
|
"form.feed.label.title": "Otsikko",
|
||||||
"form.feed.label.site_url": "Sivuston URL-osoite",
|
"form.feed.label.site_url": "Sivuston URL-osoite",
|
||||||
"form.feed.label.feed_url": "Syötteen URL-osoite",
|
"form.feed.label.feed_url": "Syötteen URL-osoite",
|
||||||
"form.feed.label.description": "Kuvaus",
|
|
||||||
"form.feed.label.category": "Kategoria",
|
"form.feed.label.category": "Kategoria",
|
||||||
"form.feed.label.crawler": "Nouda alkuperäinen sisältö",
|
"form.feed.label.crawler": "Nouda alkuperäinen sisältö",
|
||||||
"form.feed.label.feed_username": "Syötteen käyttäjätunnus",
|
"form.feed.label.feed_username": "Syötteen käyttäjätunnus",
|
||||||
@ -348,13 +333,6 @@
|
|||||||
"form.feed.label.disabled": "Älä päivitä tätä syötettä",
|
"form.feed.label.disabled": "Älä päivitä tätä syötettä",
|
||||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||||
"form.feed.label.hide_globally": "Piilota artikkelit lukemattomien listassa",
|
"form.feed.label.hide_globally": "Piilota artikkelit lukemattomien listassa",
|
||||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
|
||||||
"form.feed.fieldset.general": "General",
|
"form.feed.fieldset.general": "General",
|
||||||
"form.feed.fieldset.rules": "Rules",
|
"form.feed.fieldset.rules": "Rules",
|
||||||
"form.feed.fieldset.network_settings": "Network Settings",
|
"form.feed.fieldset.network_settings": "Network Settings",
|
||||||
@ -395,18 +373,11 @@
|
|||||||
"form.prefs.label.default_home_page": "Oletusarvoinen etusivu",
|
"form.prefs.label.default_home_page": "Oletusarvoinen etusivu",
|
||||||
"form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu",
|
"form.prefs.label.categories_sorting_order": "Kategorioiden lajittelu",
|
||||||
"form.prefs.label.mark_read_on_view": "Merkitse kohdat automaattisesti luetuiksi, kun niitä tarkastellaan",
|
"form.prefs.label.mark_read_on_view": "Merkitse kohdat automaattisesti luetuiksi, kun niitä tarkastellaan",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
|
||||||
"form.import.label.file": "OPML-tiedosto",
|
"form.import.label.file": "OPML-tiedosto",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "Save entries to Betula",
|
|
||||||
"form.integration.betula_url": "Betula server URL",
|
|
||||||
"form.integration.betula_token": "Betula Token",
|
|
||||||
"form.integration.fever_activate": "Ota Fever API käyttöön",
|
"form.integration.fever_activate": "Ota Fever API käyttöön",
|
||||||
"form.integration.fever_username": "Fever-käyttäjätunnus",
|
"form.integration.fever_username": "Fever-käyttäjätunnus",
|
||||||
"form.integration.fever_password": "Fever-salasana",
|
"form.integration.fever_password": "Fever-salasana",
|
||||||
@ -478,10 +449,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Matrix-käyttäjän salasana",
|
"form.integration.matrix_bot_password": "Matrix-käyttäjän salasana",
|
||||||
"form.integration.matrix_bot_url": "Matrix-palvelimen URL-osoite",
|
"form.integration.matrix_bot_url": "Matrix-palvelimen URL-osoite",
|
||||||
"form.integration.matrix_bot_chat_id": "Matrix-huoneen tunnus",
|
"form.integration.matrix_bot_chat_id": "Matrix-huoneen tunnus",
|
||||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "Tallenna artikkelit Readeckiin",
|
"form.integration.readeck_activate": "Tallenna artikkelit Readeckiin",
|
||||||
"form.integration.readeck_endpoint": "Readeck API-päätepiste",
|
"form.integration.readeck_endpoint": "Readeck API-päätepiste",
|
||||||
"form.integration.readeck_api_key": "Readeck API-avain",
|
"form.integration.readeck_api_key": "Readeck API-avain",
|
||||||
@ -499,13 +466,6 @@
|
|||||||
"form.integration.webhook_secret": "Webhook Secret",
|
"form.integration.webhook_secret": "Webhook Secret",
|
||||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
|
||||||
"form.api_key.label.description": "API Key Label",
|
"form.api_key.label.description": "API Key Label",
|
||||||
"form.submit.loading": "Ladataan...",
|
"form.submit.loading": "Ladataan...",
|
||||||
"form.submit.saving": "Tallennetaan...",
|
"form.submit.saving": "Tallennetaan...",
|
||||||
@ -545,7 +505,7 @@
|
|||||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP client error: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
@ -564,16 +524,5 @@
|
|||||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "Äänen/videon toistonopeus",
|
|
||||||
"error.settings_media_playback_rate_range": "Toistonopeus on alueen ulkopuolella",
|
|
||||||
"enclosure_media_controls.seek" : "Seek:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Speed:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Faster",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Slower",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "Tout marquer comme lu",
|
"menu.mark_all_as_read": "Tout marquer comme lu",
|
||||||
"menu.show_all_entries": "Afficher tous les articles",
|
"menu.show_all_entries": "Afficher tous les articles",
|
||||||
"menu.show_only_unread_entries": "Afficher uniquement les articles non lus",
|
"menu.show_only_unread_entries": "Afficher uniquement les articles non lus",
|
||||||
"menu.show_only_starred_entries": "Afficher uniquement les favoris",
|
|
||||||
"menu.refresh_feed": "Actualiser",
|
"menu.refresh_feed": "Actualiser",
|
||||||
"menu.refresh_all_feeds": "Actualiser les abonnements en arrière-plan",
|
"menu.refresh_all_feeds": "Actualiser les abonnements en arrière-plan",
|
||||||
"menu.edit_feed": "Modifier",
|
"menu.edit_feed": "Modifier",
|
||||||
@ -56,9 +55,7 @@
|
|||||||
"search.label": "Recherche",
|
"search.label": "Recherche",
|
||||||
"search.placeholder": "Recherche...",
|
"search.placeholder": "Recherche...",
|
||||||
"search.submit": "Rechercher",
|
"search.submit": "Rechercher",
|
||||||
"pagination.last": "Dernière page",
|
|
||||||
"pagination.next": "Suivant",
|
"pagination.next": "Suivant",
|
||||||
"pagination.first": "Première page",
|
|
||||||
"pagination.previous": "Précédent",
|
"pagination.previous": "Précédent",
|
||||||
"entry.status.unread": "Non lu",
|
"entry.status.unread": "Non lu",
|
||||||
"entry.status.read": "Lu",
|
"entry.status.read": "Lu",
|
||||||
@ -181,8 +178,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "Voir abonnement",
|
"page.keyboard_shortcuts.go_to_feed": "Voir abonnement",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "Page précédente",
|
"page.keyboard_shortcuts.go_to_previous_page": "Page précédente",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "Page suivante",
|
"page.keyboard_shortcuts.go_to_next_page": "Page suivante",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "Aller à l'élément du bas",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "Aller à l'élément supérieur",
|
|
||||||
"page.keyboard_shortcuts.open_item": "Ouvrir élément sélectionné",
|
"page.keyboard_shortcuts.open_item": "Ouvrir élément sélectionné",
|
||||||
"page.keyboard_shortcuts.open_original": "Ouvrir le lien original",
|
"page.keyboard_shortcuts.open_original": "Ouvrir le lien original",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "Ouvrir le lien original dans l'onglet en cours",
|
"page.keyboard_shortcuts.open_original_same_window": "Ouvrir le lien original dans l'onglet en cours",
|
||||||
@ -211,8 +206,8 @@
|
|||||||
"page.settings.title": "Réglages",
|
"page.settings.title": "Réglages",
|
||||||
"page.settings.link_google_account": "Associer mon compte Google",
|
"page.settings.link_google_account": "Associer mon compte Google",
|
||||||
"page.settings.unlink_google_account": "Dissocier mon compte Google",
|
"page.settings.unlink_google_account": "Dissocier mon compte Google",
|
||||||
"page.settings.link_oidc_account": "Associer mon compte %s",
|
"page.settings.link_oidc_account": "Associer mon compte OpenID Connect",
|
||||||
"page.settings.unlink_oidc_account": "Dissocier mon compte %s",
|
"page.settings.unlink_oidc_account": "Dissocier mon compte OpenID Connect",
|
||||||
"page.settings.webauthn.passkeys": "Clés d’accès",
|
"page.settings.webauthn.passkeys": "Clés d’accès",
|
||||||
"page.settings.webauthn.actions": "Actions",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Nom de la clé d’accès",
|
"page.settings.webauthn.passkey_name": "Nom de la clé d’accès",
|
||||||
@ -220,13 +215,13 @@
|
|||||||
"page.settings.webauthn.last_seen_on": "Dernière utilisation",
|
"page.settings.webauthn.last_seen_on": "Dernière utilisation",
|
||||||
"page.settings.webauthn.register": "Enregister une nouvelle clé d’accès",
|
"page.settings.webauthn.register": "Enregister une nouvelle clé d’accès",
|
||||||
"page.settings.webauthn.register.error": "Impossible d'enregistrer la clé d’accès",
|
"page.settings.webauthn.register.error": "Impossible d'enregistrer la clé d’accès",
|
||||||
"page.settings.webauthn.delete": [
|
"page.settings.webauthn.delete" : [
|
||||||
"Supprimer %d clé d’accès",
|
"Supprimer %d clé d’accès",
|
||||||
"Supprimer %d clés d’accès"
|
"Supprimer %d clés d’accès"
|
||||||
],
|
],
|
||||||
"page.login.title": "Connexion",
|
"page.login.title": "Connexion",
|
||||||
"page.login.google_signin": "Se connecter avec Google",
|
"page.login.google_signin": "Se connecter avec Google",
|
||||||
"page.login.oidc_signin": "Se connecter avec %s",
|
"page.login.oidc_signin": "Se connecter avec OpenID Connect",
|
||||||
"page.login.webauthn_login": "Se connecter avec une clé d’accès",
|
"page.login.webauthn_login": "Se connecter avec une clé d’accès",
|
||||||
"page.login.webauthn_login.error": "Impossible de se connecter avec la clé d’accès",
|
"page.login.webauthn_login.error": "Impossible de se connecter avec la clé d’accès",
|
||||||
"page.integrations.title": "Intégrations",
|
"page.integrations.title": "Intégrations",
|
||||||
@ -261,7 +256,6 @@
|
|||||||
"alert.no_bookmark": "Il n'y a aucun favoris pour le moment.",
|
"alert.no_bookmark": "Il n'y a aucun favoris pour le moment.",
|
||||||
"alert.no_category": "Il n'y a aucune catégorie.",
|
"alert.no_category": "Il n'y a aucune catégorie.",
|
||||||
"alert.no_category_entry": "Il n'y a aucun article dans cette catégorie.",
|
"alert.no_category_entry": "Il n'y a aucun article dans cette catégorie.",
|
||||||
"alert.no_tag_entry": "Il n'y a aucun article correspondant à ce tag.",
|
|
||||||
"alert.no_feed_entry": "Il n'y a aucun article pour cet abonnement.",
|
"alert.no_feed_entry": "Il n'y a aucun article pour cet abonnement.",
|
||||||
"alert.no_feed": "Vous n'avez aucun abonnement.",
|
"alert.no_feed": "Vous n'avez aucun abonnement.",
|
||||||
"alert.no_feed_in_category": "Il n'y a pas d'abonnement pour cette catégorie.",
|
"alert.no_feed_in_category": "Il n'y a pas d'abonnement pour cette catégorie.",
|
||||||
@ -296,14 +290,6 @@
|
|||||||
"error.password_min_length": "Vous devez utiliser au moins 6 caractères pour le mot de passe.",
|
"error.password_min_length": "Vous devez utiliser au moins 6 caractères pour le mot de passe.",
|
||||||
"error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.",
|
"error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.",
|
||||||
"error.settings_reading_speed_is_positive": "Les vitesses de lecture doivent être des entiers positifs.",
|
"error.settings_reading_speed_is_positive": "Les vitesses de lecture doivent être des entiers positifs.",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Règle de blocage invalide : la règle n°%d ne contient pas un nom de champ valide (Options : %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Règle de blocage invalide : le motif de la règle n°%d doit être séparé par un '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Règle de blocage invalide : le motif de la règle n°%d n'est pas fourni",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Règle de blocage invalide : le motif de la règle n°%d n'est pas une expression régulière valide",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Règle de conservation invalide : la règle n°%d ne contient pas un nom de champ valide (Options : %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Règle de conservation invalide : le motif de la règle n°%d doit être séparé par un '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Règle de conservation invalide : le motif de la règle n°%d n'est pas fourni",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Règle de conservation invalide : le motif de la règle n°%d n'est pas une expression régulière valide",
|
|
||||||
"error.entries_per_page_invalid": "Le nombre d'entrées par page n'est pas valide.",
|
"error.entries_per_page_invalid": "Le nombre d'entrées par page n'est pas valide.",
|
||||||
"error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.",
|
"error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.",
|
||||||
"error.feed_already_exists": "Ce flux existe déjà.",
|
"error.feed_already_exists": "Ce flux existe déjà.",
|
||||||
@ -328,7 +314,6 @@
|
|||||||
"form.feed.label.title": "Titre",
|
"form.feed.label.title": "Titre",
|
||||||
"form.feed.label.site_url": "URL du site web",
|
"form.feed.label.site_url": "URL du site web",
|
||||||
"form.feed.label.feed_url": "URL du flux",
|
"form.feed.label.feed_url": "URL du flux",
|
||||||
"form.feed.label.description": "Description",
|
|
||||||
"form.feed.label.category": "Catégorie",
|
"form.feed.label.category": "Catégorie",
|
||||||
"form.feed.label.crawler": "Récupérer le contenu original",
|
"form.feed.label.crawler": "Récupérer le contenu original",
|
||||||
"form.feed.label.feed_username": "Nom d'utilisateur du flux",
|
"form.feed.label.feed_username": "Nom d'utilisateur du flux",
|
||||||
@ -348,13 +333,6 @@
|
|||||||
"form.feed.label.disabled": "Ne pas actualiser ce flux",
|
"form.feed.label.disabled": "Ne pas actualiser ce flux",
|
||||||
"form.feed.label.no_media_player": "Pas de lecteur multimedia (audio/vidéo)",
|
"form.feed.label.no_media_player": "Pas de lecteur multimedia (audio/vidéo)",
|
||||||
"form.feed.label.hide_globally": "Masquer les entrées dans la liste globale non lue",
|
"form.feed.label.hide_globally": "Masquer les entrées dans la liste globale non lue",
|
||||||
"form.feed.label.ntfy_activate": "Activer les notifications",
|
|
||||||
"form.feed.label.ntfy_priority": "Priorité de notification",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Priorité maximale de notification",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Priorité élevée de notification",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Priorité par défaut de notification",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Priorité basse de notification",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Priorité minimale de notification",
|
|
||||||
"form.feed.fieldset.general": "Général",
|
"form.feed.fieldset.general": "Général",
|
||||||
"form.feed.fieldset.rules": "Règles",
|
"form.feed.fieldset.rules": "Règles",
|
||||||
"form.feed.fieldset.network_settings": "Paramètres réseau",
|
"form.feed.fieldset.network_settings": "Paramètres réseau",
|
||||||
@ -395,18 +373,11 @@
|
|||||||
"form.prefs.label.default_home_page": "Page d'accueil par défaut",
|
"form.prefs.label.default_home_page": "Page d'accueil par défaut",
|
||||||
"form.prefs.label.categories_sorting_order": "Colonne de tri des catégories",
|
"form.prefs.label.categories_sorting_order": "Colonne de tri des catégories",
|
||||||
"form.prefs.label.mark_read_on_view": "Marquer automatiquement les entrées comme lues lorsqu'elles sont consultées",
|
"form.prefs.label.mark_read_on_view": "Marquer automatiquement les entrées comme lues lorsqu'elles sont consultées",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Marquer automatiquement les entrées comme lues lorsqu'elles sont consultées. Pour l'audio/vidéo, marquer comme lues après 90%%",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Marqué les entrées comme lues uniquement après 90%% de lecture de l'audio/vidéo",
|
|
||||||
"form.prefs.label.mark_read_manually": "Marqué les entrées comme lues manuellement",
|
|
||||||
"form.prefs.fieldset.application_settings": "Paramètres de l'application",
|
"form.prefs.fieldset.application_settings": "Paramètres de l'application",
|
||||||
"form.prefs.fieldset.authentication_settings": "Paramètres d'authentification",
|
"form.prefs.fieldset.authentication_settings": "Paramètres d'authentification",
|
||||||
"form.prefs.fieldset.reader_settings": "Paramètres du lecteur",
|
"form.prefs.fieldset.reader_settings": "Paramètres du lecteur",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Paramètres globaux des abonnements",
|
|
||||||
"form.import.label.file": "Fichier OPML",
|
"form.import.label.file": "Fichier OPML",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "Sauvegarder les entrées vers Betula",
|
|
||||||
"form.integration.betula_url": "URL du serveur Betula",
|
|
||||||
"form.integration.betula_token": "Jeton de sécurité de l'API de Betula",
|
|
||||||
"form.integration.fever_activate": "Activer l'API de Fever",
|
"form.integration.fever_activate": "Activer l'API de Fever",
|
||||||
"form.integration.fever_username": "Nom d'utilisateur pour l'API de Fever",
|
"form.integration.fever_username": "Nom d'utilisateur pour l'API de Fever",
|
||||||
"form.integration.fever_password": "Mot de passe pour l'API de Fever",
|
"form.integration.fever_password": "Mot de passe pour l'API de Fever",
|
||||||
@ -449,7 +420,7 @@
|
|||||||
"form.integration.espial_endpoint": "URL de l'API de Espial",
|
"form.integration.espial_endpoint": "URL de l'API de Espial",
|
||||||
"form.integration.espial_api_key": "Clé d'API de Espial",
|
"form.integration.espial_api_key": "Clé d'API de Espial",
|
||||||
"form.integration.espial_tags": "Libellés de Espial",
|
"form.integration.espial_tags": "Libellés de Espial",
|
||||||
"form.integration.readwise_activate": "Enregistrer les entrées vers Readwise Reader",
|
"form.integration.readwise_activate": "Enregistrer les entrées dans Readwise Reader",
|
||||||
"form.integration.readwise_api_key": "Jeton d'accès au lecteur Readwise",
|
"form.integration.readwise_api_key": "Jeton d'accès au lecteur Readwise",
|
||||||
"form.integration.readwise_api_key_link": "Obtenez votre jeton d'accès Readwise",
|
"form.integration.readwise_api_key_link": "Obtenez votre jeton d'accès Readwise",
|
||||||
"form.integration.telegram_bot_activate": "Envoyer les nouveaux articles vers Telegram",
|
"form.integration.telegram_bot_activate": "Envoyer les nouveaux articles vers Telegram",
|
||||||
@ -458,17 +429,17 @@
|
|||||||
"form.integration.telegram_topic_id": "Identifiant du sujet (Topic ID)",
|
"form.integration.telegram_topic_id": "Identifiant du sujet (Topic ID)",
|
||||||
"form.integration.telegram_bot_disable_web_page_preview": "Désactiver l'aperçu de la page Web",
|
"form.integration.telegram_bot_disable_web_page_preview": "Désactiver l'aperçu de la page Web",
|
||||||
"form.integration.telegram_bot_disable_notification": "Désactiver les notifications",
|
"form.integration.telegram_bot_disable_notification": "Désactiver les notifications",
|
||||||
"form.integration.telegram_bot_disable_buttons": "Désactiver les boutons",
|
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||||
"form.integration.linkace_activate": "Enregistrer les entrées vers LinkAce",
|
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||||
"form.integration.linkace_endpoint": "Point de terminaison de l'API LinkAce",
|
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||||
"form.integration.linkace_api_key": "Clé d'API LinkAce",
|
"form.integration.linkace_api_key": "LinkAce API key",
|
||||||
"form.integration.linkace_tags": "Étiquettes LinkAce",
|
"form.integration.linkace_tags": "LinkAce Tags",
|
||||||
"form.integration.linkace_is_private": "Marquer le lien comme privé",
|
"form.integration.linkace_is_private": "Mark link as private",
|
||||||
"form.integration.linkace_check_disabled": "Désactiver la vérification des liens",
|
"form.integration.linkace_check_disabled": "Disable link check",
|
||||||
"form.integration.linkding_activate": "Sauvegarder les articles vers Linkding",
|
"form.integration.linkding_activate": "Sauvegarder les articles vers Linkding",
|
||||||
"form.integration.linkding_endpoint": "URL de l'API de Linkding",
|
"form.integration.linkding_endpoint": "URL de l'API de Linkding",
|
||||||
"form.integration.linkding_api_key": "Clé d'API de Linkding",
|
"form.integration.linkding_api_key": "Clé d'API de Linkding",
|
||||||
"form.integration.linkding_tags": "Libellés",
|
"form.integration.linkding_tags": "Linkding Tags",
|
||||||
"form.integration.linkding_bookmark": "Marquer le lien comme non lu",
|
"form.integration.linkding_bookmark": "Marquer le lien comme non lu",
|
||||||
"form.integration.linkwarden_activate": "Sauvegarder les articles vers Linkwarden",
|
"form.integration.linkwarden_activate": "Sauvegarder les articles vers Linkwarden",
|
||||||
"form.integration.linkwarden_endpoint": "URL de l'API de Linkwarden",
|
"form.integration.linkwarden_endpoint": "URL de l'API de Linkwarden",
|
||||||
@ -478,10 +449,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Mot de passe de l'utilisateur Matrix",
|
"form.integration.matrix_bot_password": "Mot de passe de l'utilisateur Matrix",
|
||||||
"form.integration.matrix_bot_url": "URL du serveur Matrix",
|
"form.integration.matrix_bot_url": "URL du serveur Matrix",
|
||||||
"form.integration.matrix_bot_chat_id": "Identifiant de la salle Matrix",
|
"form.integration.matrix_bot_chat_id": "Identifiant de la salle Matrix",
|
||||||
"form.integration.raindrop_activate": "Enregistrer les entrées vers Raindrop",
|
|
||||||
"form.integration.raindrop_token": "Jeton d'accès de Raindrop",
|
|
||||||
"form.integration.raindrop_collection_id": "Identifiant de la collection",
|
|
||||||
"form.integration.raindrop_tags": "Libellés (séparées par des virgules)",
|
|
||||||
"form.integration.readeck_activate": "Sauvegarder les articles vers Readeck",
|
"form.integration.readeck_activate": "Sauvegarder les articles vers Readeck",
|
||||||
"form.integration.readeck_endpoint": "URL de l'API de Readeck",
|
"form.integration.readeck_endpoint": "URL de l'API de Readeck",
|
||||||
"form.integration.readeck_api_key": "Clé d'API de Readeck",
|
"form.integration.readeck_api_key": "Clé d'API de Readeck",
|
||||||
@ -499,13 +466,6 @@
|
|||||||
"form.integration.webhook_secret": "Secret du webhook",
|
"form.integration.webhook_secret": "Secret du webhook",
|
||||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Envoyer les entrées vers ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Sujet Ntfy",
|
|
||||||
"form.integration.ntfy_url": "URL de Ntfy (optionnel, ntfy.sh par défaut)",
|
|
||||||
"form.integration.ntfy_api_token": "Jeton d'API Ntfy (optionnel)",
|
|
||||||
"form.integration.ntfy_username": "Nom d'utilisateur Ntfy (optionnel)",
|
|
||||||
"form.integration.ntfy_password": "Mot de passe Ntfy (facultatif)",
|
|
||||||
"form.integration.ntfy_icon_url": "URL de l'icône Ntfy (facultatif)",
|
|
||||||
"form.api_key.label.description": "Libellé de la clé d'API",
|
"form.api_key.label.description": "Libellé de la clé d'API",
|
||||||
"form.submit.loading": "Chargement...",
|
"form.submit.loading": "Chargement...",
|
||||||
"form.submit.saving": "Sauvegarde en cours...",
|
"form.submit.saving": "Sauvegarde en cours...",
|
||||||
@ -545,7 +505,7 @@
|
|||||||
"error.http_body_read": "Impossible de lire le corps de la réponse HTTP : %v.",
|
"error.http_body_read": "Impossible de lire le corps de la réponse HTTP : %v.",
|
||||||
"error.http_empty_response_body": "Le corps de la réponse HTTP est vide.",
|
"error.http_empty_response_body": "Le corps de la réponse HTTP est vide.",
|
||||||
"error.http_empty_response": "La réponse HTTP est vide. Peut-être que ce site web bloque Miniflux avec une protection anti-bot ?",
|
"error.http_empty_response": "La réponse HTTP est vide. Peut-être que ce site web bloque Miniflux avec une protection anti-bot ?",
|
||||||
"error.tls_error": "Erreur TLS : %q. Vous pouvez désactiver la vérification TLS dans les paramètres de l'abonnement.",
|
"error.tls_error": "Erreur TLS : %v. Vous pouvez désactiver la vérification TLS dans les paramètres de l'abonnement.",
|
||||||
"error.network_operation": "Miniflux n'est pas en mesure de se connecter à ce site web à cause d'un problème réseau : %v.",
|
"error.network_operation": "Miniflux n'est pas en mesure de se connecter à ce site web à cause d'un problème réseau : %v.",
|
||||||
"error.network_timeout": "Ce site web est trop lent à répondre : %v.",
|
"error.network_timeout": "Ce site web est trop lent à répondre : %v.",
|
||||||
"error.http_client_error": "Erreur du client HTTP : %v.",
|
"error.http_client_error": "Erreur du client HTTP : %v.",
|
||||||
@ -564,16 +524,5 @@
|
|||||||
"error.unable_to_parse_feed": "Impossible d'analyser ce flux : %v.",
|
"error.unable_to_parse_feed": "Impossible d'analyser ce flux : %v.",
|
||||||
"error.feed_not_found": "Impossible de trouver ce flux.",
|
"error.feed_not_found": "Impossible de trouver ce flux.",
|
||||||
"error.unable_to_detect_rssbridge": "Impossible de détecter un flux RSS en utilisant RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Impossible de détecter un flux RSS en utilisant RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Impossible de détecter le format du flux : %v.",
|
"error.feed_format_not_detected": "Impossible de détecter le format du flux : %v."
|
||||||
"form.prefs.label.media_playback_rate": "Vitesse de lecture de l'audio/vidéo",
|
|
||||||
"error.settings_media_playback_rate_range": "La vitesse de lecture est hors limites",
|
|
||||||
"enclosure_media_controls.seek" : "Avancer/Reculer :",
|
|
||||||
"enclosure_media_controls.seek.title" : "Avancer/Reculer de %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Vitesse :",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Accélérer",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Accélérer de %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Ralentir",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Ralentir de %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Réinitialiser",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Réinitialiser la vitesse de lecture à 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "सभी को पढ़ा हुआ मार्क करें",
|
"menu.mark_all_as_read": "सभी को पढ़ा हुआ मार्क करें",
|
||||||
"menu.show_all_entries": "सभी प्रविष्टियाँ दिखाए",
|
"menu.show_all_entries": "सभी प्रविष्टियाँ दिखाए",
|
||||||
"menu.show_only_unread_entries": "सभी अपठित प्रविष्टियाँ दिखाए",
|
"menu.show_only_unread_entries": "सभी अपठित प्रविष्टियाँ दिखाए",
|
||||||
"menu.show_only_starred_entries": "Show only starred entries",
|
|
||||||
"menu.refresh_feed": "ताज़ा करें",
|
"menu.refresh_feed": "ताज़ा करें",
|
||||||
"menu.refresh_all_feeds": "पृष्ठभूमि में सभी फ़ीड को ताज़ा करें",
|
"menu.refresh_all_feeds": "पृष्ठभूमि में सभी फ़ीड को ताज़ा करें",
|
||||||
"menu.edit_feed": "फ़ीड संपाद करे",
|
"menu.edit_feed": "फ़ीड संपाद करे",
|
||||||
@ -56,9 +55,7 @@
|
|||||||
"search.label": "खोजे",
|
"search.label": "खोजे",
|
||||||
"search.placeholder": "खोजे...",
|
"search.placeholder": "खोजे...",
|
||||||
"search.submit": "Search",
|
"search.submit": "Search",
|
||||||
"pagination.last": "Last",
|
|
||||||
"pagination.next": "अगला",
|
"pagination.next": "अगला",
|
||||||
"pagination.first": "First",
|
|
||||||
"pagination.previous": "पिछला",
|
"pagination.previous": "पिछला",
|
||||||
"entry.status.unread": "अपठित",
|
"entry.status.unread": "अपठित",
|
||||||
"entry.status.read": "पढ़े",
|
"entry.status.read": "पढ़े",
|
||||||
@ -181,8 +178,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "फ़ीड पर जाएं",
|
"page.keyboard_shortcuts.go_to_feed": "फ़ीड पर जाएं",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "पिछले पृष्ठ पर जाएं",
|
"page.keyboard_shortcuts.go_to_previous_page": "पिछले पृष्ठ पर जाएं",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "अगले पेज पर जाएं",
|
"page.keyboard_shortcuts.go_to_next_page": "अगले पेज पर जाएं",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "निचले आइटम पर जाएँ",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "शीर्ष आइटम पर जाएँ",
|
|
||||||
"page.keyboard_shortcuts.open_item": "चयनित आइटम खोलें",
|
"page.keyboard_shortcuts.open_item": "चयनित आइटम खोलें",
|
||||||
"page.keyboard_shortcuts.open_original": "मूल लिंक खोलें",
|
"page.keyboard_shortcuts.open_original": "मूल लिंक खोलें",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "वर्तमान टैब में मूल लिंक खोलें",
|
"page.keyboard_shortcuts.open_original_same_window": "वर्तमान टैब में मूल लिंक खोलें",
|
||||||
@ -211,8 +206,8 @@
|
|||||||
"page.settings.title": "समायोजन",
|
"page.settings.title": "समायोजन",
|
||||||
"page.settings.link_google_account": "मेरा गूगल खाता जोरीय",
|
"page.settings.link_google_account": "मेरा गूगल खाता जोरीय",
|
||||||
"page.settings.unlink_google_account": "मेरा गूगल खाता हटाय",
|
"page.settings.unlink_google_account": "मेरा गूगल खाता हटाय",
|
||||||
"page.settings.link_oidc_account": "मेरा ओपन-ईद खाता जोरीय (%s)",
|
"page.settings.link_oidc_account": "मेरा ओपन-ईद खाता जोरीय",
|
||||||
"page.settings.unlink_oidc_account": "मेरा ओपन-ईद खाता हटाय (%s)",
|
"page.settings.unlink_oidc_account": "मेरा ओपन-ईद खाता हटाय",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Actions",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||||
@ -226,7 +221,7 @@
|
|||||||
],
|
],
|
||||||
"page.login.title": "साइन इन करें",
|
"page.login.title": "साइन इन करें",
|
||||||
"page.login.google_signin": "गूगल के साथ साइन इन करें",
|
"page.login.google_signin": "गूगल के साथ साइन इन करें",
|
||||||
"page.login.oidc_signin": "ओपन-ईद के साथ साइन इन करें (%s)",
|
"page.login.oidc_signin": "ओपन-ईद के साथ साइन इन करें",
|
||||||
"page.login.webauthn_login": "पासकी से लॉगिन करें",
|
"page.login.webauthn_login": "पासकी से लॉगिन करें",
|
||||||
"page.login.webauthn_login.error": "पासकी से लॉगिन करने में असमर्थ",
|
"page.login.webauthn_login.error": "पासकी से लॉगिन करने में असमर्थ",
|
||||||
"page.integrations.title": "एकीकरण",
|
"page.integrations.title": "एकीकरण",
|
||||||
@ -261,7 +256,6 @@
|
|||||||
"alert.no_bookmark": "इस समय कोई बुकमार्क नहीं है",
|
"alert.no_bookmark": "इस समय कोई बुकमार्क नहीं है",
|
||||||
"alert.no_category": "कोई श्रेणी नहीं है।",
|
"alert.no_category": "कोई श्रेणी नहीं है।",
|
||||||
"alert.no_category_entry": "इस श्रेणी में कोई विषय-वस्तु नहीं है।",
|
"alert.no_category_entry": "इस श्रेणी में कोई विषय-वस्तु नहीं है।",
|
||||||
"alert.no_tag_entry": "इस टैग से मेल खाती कोई प्रविष्टियाँ नहीं हैं।",
|
|
||||||
"alert.no_feed_entry": "इस फ़ीड के लिए कोई विषय-वस्तु नहीं है।",
|
"alert.no_feed_entry": "इस फ़ीड के लिए कोई विषय-वस्तु नहीं है।",
|
||||||
"alert.no_feed": "आपके पास कोई सदस्यता नहीं है।",
|
"alert.no_feed": "आपके पास कोई सदस्यता नहीं है।",
|
||||||
"alert.no_feed_in_category": "इस श्रेणी के लिए कोई सदस्यता नहीं है।",
|
"alert.no_feed_in_category": "इस श्रेणी के लिए कोई सदस्यता नहीं है।",
|
||||||
@ -303,14 +297,6 @@
|
|||||||
"error.password_min_length": "पासवर्ड में कम से कम 6 अक्षर होने चाहिए।",
|
"error.password_min_length": "पासवर्ड में कम से कम 6 अक्षर होने चाहिए।",
|
||||||
"error.settings_mandatory_fields": "उपयोगकर्ता नाम, विषयवस्तु, भाषा और समयक्षेत्र फ़ील्ड अनिवार्य हैं।",
|
"error.settings_mandatory_fields": "उपयोगकर्ता नाम, विषयवस्तु, भाषा और समयक्षेत्र फ़ील्ड अनिवार्य हैं।",
|
||||||
"error.settings_reading_speed_is_positive": "पढ़ने की गति सकारात्मक पूर्णांक होनी चाहिए।",
|
"error.settings_reading_speed_is_positive": "पढ़ने की गति सकारात्मक पूर्णांक होनी चाहिए।",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.entries_per_page_invalid": "प्रति पृष्ठ प्रविष्टियों की संख्या मान्य नहीं है।",
|
"error.entries_per_page_invalid": "प्रति पृष्ठ प्रविष्टियों की संख्या मान्य नहीं है।",
|
||||||
"error.feed_mandatory_fields": "URL और श्रेणी अनिवार्य हैं।",
|
"error.feed_mandatory_fields": "URL और श्रेणी अनिवार्य हैं।",
|
||||||
"error.feed_already_exists": "यह फ़ीड पहले से मौजूद है.",
|
"error.feed_already_exists": "यह फ़ीड पहले से मौजूद है.",
|
||||||
@ -328,7 +314,6 @@
|
|||||||
"form.feed.label.title": "शीर्षक",
|
"form.feed.label.title": "शीर्षक",
|
||||||
"form.feed.label.site_url": "साइट यूआरएल",
|
"form.feed.label.site_url": "साइट यूआरएल",
|
||||||
"form.feed.label.feed_url": "फ़ीड यूआरएल",
|
"form.feed.label.feed_url": "फ़ीड यूआरएल",
|
||||||
"form.feed.label.description": "विवरण",
|
|
||||||
"form.feed.label.category": "श्रेणी",
|
"form.feed.label.category": "श्रेणी",
|
||||||
"form.feed.label.crawler": "मूल सामग्री प्राप्त करें",
|
"form.feed.label.crawler": "मूल सामग्री प्राप्त करें",
|
||||||
"form.feed.label.feed_username": "फ़ीड उपयोगकर्ता नाम",
|
"form.feed.label.feed_username": "फ़ीड उपयोगकर्ता नाम",
|
||||||
@ -348,13 +333,6 @@
|
|||||||
"form.feed.label.disabled": "इस फ़ीड को रीफ़्रेश न करें",
|
"form.feed.label.disabled": "इस फ़ीड को रीफ़्रेश न करें",
|
||||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||||
"form.feed.label.hide_globally": "वैश्विक अपठित सूची में प्रविष्टियां छिपाएं",
|
"form.feed.label.hide_globally": "वैश्विक अपठित सूची में प्रविष्टियां छिपाएं",
|
||||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
|
||||||
"form.feed.fieldset.general": "General",
|
"form.feed.fieldset.general": "General",
|
||||||
"form.feed.fieldset.rules": "Rules",
|
"form.feed.fieldset.rules": "Rules",
|
||||||
"form.feed.fieldset.network_settings": "Network Settings",
|
"form.feed.fieldset.network_settings": "Network Settings",
|
||||||
@ -395,18 +373,11 @@
|
|||||||
"form.prefs.label.default_home_page": "डिफ़ॉल्ट होमपेज़",
|
"form.prefs.label.default_home_page": "डिफ़ॉल्ट होमपेज़",
|
||||||
"form.prefs.label.categories_sorting_order": "श्रेणियाँ छँटाई",
|
"form.prefs.label.categories_sorting_order": "श्रेणियाँ छँटाई",
|
||||||
"form.prefs.label.mark_read_on_view": "देखे जाने पर स्वचालित रूप से प्रविष्टियों को पढ़ने के रूप में चिह्नित करें",
|
"form.prefs.label.mark_read_on_view": "देखे जाने पर स्वचालित रूप से प्रविष्टियों को पढ़ने के रूप में चिह्नित करें",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
|
||||||
"form.import.label.file": "ओपीएमएल फ़ाइल",
|
"form.import.label.file": "ओपीएमएल फ़ाइल",
|
||||||
"form.import.label.url": "यूआरएल",
|
"form.import.label.url": "यूआरएल",
|
||||||
"form.integration.betula_activate": "Save entries to Betula",
|
|
||||||
"form.integration.betula_url": "Betula server URL",
|
|
||||||
"form.integration.betula_token": "Betula Token",
|
|
||||||
"form.integration.fever_activate": "फीवर एपीआई सक्रिय करें",
|
"form.integration.fever_activate": "फीवर एपीआई सक्रिय करें",
|
||||||
"form.integration.fever_username": "फीवर उपयोगकर्ता नाम",
|
"form.integration.fever_username": "फीवर उपयोगकर्ता नाम",
|
||||||
"form.integration.fever_password": "फीवर पासवर्ड",
|
"form.integration.fever_password": "फीवर पासवर्ड",
|
||||||
@ -478,10 +449,6 @@
|
|||||||
"form.integration.matrix_bot_password": "मैट्रिक्स उपयोगकर्ता के लिए पासवर्ड",
|
"form.integration.matrix_bot_password": "मैट्रिक्स उपयोगकर्ता के लिए पासवर्ड",
|
||||||
"form.integration.matrix_bot_url": "मैट्रिक्स सर्वर URL",
|
"form.integration.matrix_bot_url": "मैट्रिक्स सर्वर URL",
|
||||||
"form.integration.matrix_bot_chat_id": "मैट्रिक्स रूम की आईडी",
|
"form.integration.matrix_bot_chat_id": "मैट्रिक्स रूम की आईडी",
|
||||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "Readeck में विषयवस्तु सहेजें",
|
"form.integration.readeck_activate": "Readeck में विषयवस्तु सहेजें",
|
||||||
"form.integration.readeck_endpoint": "Readeck·एपीआई·समापन·बिंदु",
|
"form.integration.readeck_endpoint": "Readeck·एपीआई·समापन·बिंदु",
|
||||||
"form.integration.readeck_api_key": "Readeck एपीआई कुंजी",
|
"form.integration.readeck_api_key": "Readeck एपीआई कुंजी",
|
||||||
@ -499,13 +466,6 @@
|
|||||||
"form.integration.webhook_secret": "Webhook Secret",
|
"form.integration.webhook_secret": "Webhook Secret",
|
||||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
|
||||||
"form.api_key.label.description": "एपीआई कुंजी लेबल",
|
"form.api_key.label.description": "एपीआई कुंजी लेबल",
|
||||||
"form.submit.loading": "लोड हो रहा है...",
|
"form.submit.loading": "लोड हो रहा है...",
|
||||||
"form.submit.saving": "सहेजा जा रहा है...",
|
"form.submit.saving": "सहेजा जा रहा है...",
|
||||||
@ -545,7 +505,7 @@
|
|||||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP client error: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
@ -564,16 +524,5 @@
|
|||||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "ऑडियो/वीडियो की प्लेबैक गति",
|
|
||||||
"error.settings_media_playback_rate_range": "प्लेबैक गति सीमा से बाहर है",
|
|
||||||
"enclosure_media_controls.seek" : "Seek:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Speed:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Faster",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Slower",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "Tandai semua sebagai telah dibaca",
|
"menu.mark_all_as_read": "Tandai semua sebagai telah dibaca",
|
||||||
"menu.show_all_entries": "Tampilkan semua entri",
|
"menu.show_all_entries": "Tampilkan semua entri",
|
||||||
"menu.show_only_unread_entries": "Tampilkan hanya entri yang belum dibaca",
|
"menu.show_only_unread_entries": "Tampilkan hanya entri yang belum dibaca",
|
||||||
"menu.show_only_starred_entries": "Show only starred entries",
|
|
||||||
"menu.refresh_feed": "Muat ulang",
|
"menu.refresh_feed": "Muat ulang",
|
||||||
"menu.refresh_all_feeds": "Muat ulang semua umpan di latar belakang",
|
"menu.refresh_all_feeds": "Muat ulang semua umpan di latar belakang",
|
||||||
"menu.edit_feed": "Sunting",
|
"menu.edit_feed": "Sunting",
|
||||||
@ -57,8 +56,6 @@
|
|||||||
"search.placeholder": "Cari...",
|
"search.placeholder": "Cari...",
|
||||||
"search.submit": "Search",
|
"search.submit": "Search",
|
||||||
"pagination.next": "Berikutnya",
|
"pagination.next": "Berikutnya",
|
||||||
"pagination.last": "Last",
|
|
||||||
"pagination.first": "First",
|
|
||||||
"pagination.previous": "Sebelumnya",
|
"pagination.previous": "Sebelumnya",
|
||||||
"entry.status.unread": "Belum dibaca",
|
"entry.status.unread": "Belum dibaca",
|
||||||
"entry.status.read": "Telah dibaca",
|
"entry.status.read": "Telah dibaca",
|
||||||
@ -172,8 +169,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "Ke umpan",
|
"page.keyboard_shortcuts.go_to_feed": "Ke umpan",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "Ke halaman sebelumnya",
|
"page.keyboard_shortcuts.go_to_previous_page": "Ke halaman sebelumnya",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "Ke halaman berikutnya",
|
"page.keyboard_shortcuts.go_to_next_page": "Ke halaman berikutnya",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "Pergi ke item paling bawah",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "Pergi ke item teratas",
|
|
||||||
"page.keyboard_shortcuts.open_item": "Buka entri yang dipilih",
|
"page.keyboard_shortcuts.open_item": "Buka entri yang dipilih",
|
||||||
"page.keyboard_shortcuts.open_original": "Buka tautan asli",
|
"page.keyboard_shortcuts.open_original": "Buka tautan asli",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "Buka tautan asli di bilah saat ini",
|
"page.keyboard_shortcuts.open_original_same_window": "Buka tautan asli di bilah saat ini",
|
||||||
@ -202,8 +197,8 @@
|
|||||||
"page.settings.title": "Pengaturan",
|
"page.settings.title": "Pengaturan",
|
||||||
"page.settings.link_google_account": "Tautkan akun Google saya",
|
"page.settings.link_google_account": "Tautkan akun Google saya",
|
||||||
"page.settings.unlink_google_account": "Putuskan akun Google saya",
|
"page.settings.unlink_google_account": "Putuskan akun Google saya",
|
||||||
"page.settings.link_oidc_account": "Tautkan akun %s saya",
|
"page.settings.link_oidc_account": "Tautkan akun OpenID Connect saya",
|
||||||
"page.settings.unlink_oidc_account": "Putuskan akun %s saya",
|
"page.settings.unlink_oidc_account": "Putuskan akun OpenID Connect saya",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Actions",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||||
@ -216,7 +211,7 @@
|
|||||||
],
|
],
|
||||||
"page.login.title": "Masuk",
|
"page.login.title": "Masuk",
|
||||||
"page.login.google_signin": "Masuk dengan Google",
|
"page.login.google_signin": "Masuk dengan Google",
|
||||||
"page.login.oidc_signin": "Masuk dengan %s",
|
"page.login.oidc_signin": "Masuk dengan OpenID Connect",
|
||||||
"page.login.webauthn_login": "Login with passkey",
|
"page.login.webauthn_login": "Login with passkey",
|
||||||
"page.login.webauthn_login.error": "Unable to login with passkey",
|
"page.login.webauthn_login.error": "Unable to login with passkey",
|
||||||
"page.integrations.title": "Integrasi",
|
"page.integrations.title": "Integrasi",
|
||||||
@ -251,7 +246,6 @@
|
|||||||
"alert.no_bookmark": "Tidak ada markah.",
|
"alert.no_bookmark": "Tidak ada markah.",
|
||||||
"alert.no_category": "Tidak ada kategori.",
|
"alert.no_category": "Tidak ada kategori.",
|
||||||
"alert.no_category_entry": "Tidak ada artikel di kategori ini.",
|
"alert.no_category_entry": "Tidak ada artikel di kategori ini.",
|
||||||
"alert.no_tag_entry": "Tidak ada entri yang cocok dengan tag ini.",
|
|
||||||
"alert.no_feed_entry": "Tidak ada artikel di umpan ini.",
|
"alert.no_feed_entry": "Tidak ada artikel di umpan ini.",
|
||||||
"alert.no_feed": "Anda tidak memiliki langganan.",
|
"alert.no_feed": "Anda tidak memiliki langganan.",
|
||||||
"alert.no_feed_in_category": "Tidak ada langganan untuk kategori ini.",
|
"alert.no_feed_in_category": "Tidak ada langganan untuk kategori ini.",
|
||||||
@ -293,14 +287,6 @@
|
|||||||
"error.password_min_length": "Kata sandi harus memiliki setidaknya 6 karakter.",
|
"error.password_min_length": "Kata sandi harus memiliki setidaknya 6 karakter.",
|
||||||
"error.settings_mandatory_fields": "Harus ada nama pengguna, tema, bahasa, dan zona waktu.",
|
"error.settings_mandatory_fields": "Harus ada nama pengguna, tema, bahasa, dan zona waktu.",
|
||||||
"error.settings_reading_speed_is_positive": "Kecepatan membaca harus integer positif.",
|
"error.settings_reading_speed_is_positive": "Kecepatan membaca harus integer positif.",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.entries_per_page_invalid": "Jumlah entri per halaman tidak valid.",
|
"error.entries_per_page_invalid": "Jumlah entri per halaman tidak valid.",
|
||||||
"error.feed_mandatory_fields": "Harus ada URL dan kategorinya.",
|
"error.feed_mandatory_fields": "Harus ada URL dan kategorinya.",
|
||||||
"error.feed_already_exists": "Umpan ini sudah ada.",
|
"error.feed_already_exists": "Umpan ini sudah ada.",
|
||||||
@ -318,7 +304,6 @@
|
|||||||
"form.feed.label.title": "Judul",
|
"form.feed.label.title": "Judul",
|
||||||
"form.feed.label.site_url": "URL Situs",
|
"form.feed.label.site_url": "URL Situs",
|
||||||
"form.feed.label.feed_url": "URL Umpan",
|
"form.feed.label.feed_url": "URL Umpan",
|
||||||
"form.feed.label.description": "Deskripsi",
|
|
||||||
"form.feed.label.category": "Kategori",
|
"form.feed.label.category": "Kategori",
|
||||||
"form.feed.label.crawler": "Ambil konten asli",
|
"form.feed.label.crawler": "Ambil konten asli",
|
||||||
"form.feed.label.feed_username": "Nama Pengguna Umpan",
|
"form.feed.label.feed_username": "Nama Pengguna Umpan",
|
||||||
@ -338,13 +323,6 @@
|
|||||||
"form.feed.label.disabled": "Jangan perbarui umpan ini",
|
"form.feed.label.disabled": "Jangan perbarui umpan ini",
|
||||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||||
"form.feed.label.hide_globally": "Sembunyikan entri di daftar belum dibaca global",
|
"form.feed.label.hide_globally": "Sembunyikan entri di daftar belum dibaca global",
|
||||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
|
||||||
"form.feed.fieldset.general": "General",
|
"form.feed.fieldset.general": "General",
|
||||||
"form.feed.fieldset.rules": "Rules",
|
"form.feed.fieldset.rules": "Rules",
|
||||||
"form.feed.fieldset.network_settings": "Network Settings",
|
"form.feed.fieldset.network_settings": "Network Settings",
|
||||||
@ -385,18 +363,11 @@
|
|||||||
"form.prefs.label.default_home_page": "Beranda Baku",
|
"form.prefs.label.default_home_page": "Beranda Baku",
|
||||||
"form.prefs.label.categories_sorting_order": "Pengurutan Kategori",
|
"form.prefs.label.categories_sorting_order": "Pengurutan Kategori",
|
||||||
"form.prefs.label.mark_read_on_view": "Secara otomatis menandai entri sebagai telah dibaca saat dilihat",
|
"form.prefs.label.mark_read_on_view": "Secara otomatis menandai entri sebagai telah dibaca saat dilihat",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
|
||||||
"form.import.label.file": "Berkas OPML",
|
"form.import.label.file": "Berkas OPML",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "Save entries to Betula",
|
|
||||||
"form.integration.betula_url": "Betula server URL",
|
|
||||||
"form.integration.betula_token": "Betula Token",
|
|
||||||
"form.integration.fever_activate": "Aktifkan API Fever",
|
"form.integration.fever_activate": "Aktifkan API Fever",
|
||||||
"form.integration.fever_username": "Nama Pengguna Fever",
|
"form.integration.fever_username": "Nama Pengguna Fever",
|
||||||
"form.integration.fever_password": "Kata Sandi Fever",
|
"form.integration.fever_password": "Kata Sandi Fever",
|
||||||
@ -468,10 +439,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Kata Sandi Matrix",
|
"form.integration.matrix_bot_password": "Kata Sandi Matrix",
|
||||||
"form.integration.matrix_bot_url": "URL Peladen Matrix",
|
"form.integration.matrix_bot_url": "URL Peladen Matrix",
|
||||||
"form.integration.matrix_bot_chat_id": "ID Ruang Matrix",
|
"form.integration.matrix_bot_chat_id": "ID Ruang Matrix",
|
||||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "Simpan artikel ke Readeck",
|
"form.integration.readeck_activate": "Simpan artikel ke Readeck",
|
||||||
"form.integration.readeck_endpoint": "Titik URL API Readeck",
|
"form.integration.readeck_endpoint": "Titik URL API Readeck",
|
||||||
"form.integration.readeck_api_key": "Kunci API Readeck",
|
"form.integration.readeck_api_key": "Kunci API Readeck",
|
||||||
@ -489,13 +456,6 @@
|
|||||||
"form.integration.webhook_secret": "Webhook Secret",
|
"form.integration.webhook_secret": "Webhook Secret",
|
||||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
|
||||||
"form.api_key.label.description": "Label Kunci API",
|
"form.api_key.label.description": "Label Kunci API",
|
||||||
"form.submit.loading": "Memuat...",
|
"form.submit.loading": "Memuat...",
|
||||||
"form.submit.saving": "Menyimpan...",
|
"form.submit.saving": "Menyimpan...",
|
||||||
@ -528,7 +488,7 @@
|
|||||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP client error: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
@ -547,16 +507,5 @@
|
|||||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "Kecepatan pemutaran audio/video",
|
|
||||||
"error.settings_media_playback_rate_range": "Kecepatan pemutaran di luar jangkauan",
|
|
||||||
"enclosure_media_controls.seek" : "Seek:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Speed:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Faster",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Slower",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "Segna tutti gli articoli come letti",
|
"menu.mark_all_as_read": "Segna tutti gli articoli come letti",
|
||||||
"menu.show_all_entries": "Mostra tutte le voci",
|
"menu.show_all_entries": "Mostra tutte le voci",
|
||||||
"menu.show_only_unread_entries": "Mostra solo voci non lette",
|
"menu.show_only_unread_entries": "Mostra solo voci non lette",
|
||||||
"menu.show_only_starred_entries": "Mostra solo voci preferiti",
|
|
||||||
"menu.refresh_feed": "Aggiorna",
|
"menu.refresh_feed": "Aggiorna",
|
||||||
"menu.refresh_all_feeds": "Aggiorna tutti i feed in background",
|
"menu.refresh_all_feeds": "Aggiorna tutti i feed in background",
|
||||||
"menu.edit_feed": "Modifica",
|
"menu.edit_feed": "Modifica",
|
||||||
@ -57,8 +56,6 @@
|
|||||||
"search.placeholder": "Cerca...",
|
"search.placeholder": "Cerca...",
|
||||||
"search.submit": "Search",
|
"search.submit": "Search",
|
||||||
"pagination.next": "Successivo",
|
"pagination.next": "Successivo",
|
||||||
"pagination.last": "Last",
|
|
||||||
"pagination.first": "First",
|
|
||||||
"pagination.previous": "Precedente",
|
"pagination.previous": "Precedente",
|
||||||
"entry.status.unread": "Da leggere",
|
"entry.status.unread": "Da leggere",
|
||||||
"entry.status.read": "Letto",
|
"entry.status.read": "Letto",
|
||||||
@ -181,8 +178,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "Mostra il feed",
|
"page.keyboard_shortcuts.go_to_feed": "Mostra il feed",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "Mostra la pagina precedente",
|
"page.keyboard_shortcuts.go_to_previous_page": "Mostra la pagina precedente",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "Mostra la pagina successiva",
|
"page.keyboard_shortcuts.go_to_next_page": "Mostra la pagina successiva",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "Vai all'elemento in fondo",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "Vai all'elemento principale",
|
|
||||||
"page.keyboard_shortcuts.open_item": "Apri l'articolo selezionato",
|
"page.keyboard_shortcuts.open_item": "Apri l'articolo selezionato",
|
||||||
"page.keyboard_shortcuts.open_original": "Apri la pagina web originale",
|
"page.keyboard_shortcuts.open_original": "Apri la pagina web originale",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "Apri il link originale nella scheda corrente",
|
"page.keyboard_shortcuts.open_original_same_window": "Apri il link originale nella scheda corrente",
|
||||||
@ -211,8 +206,8 @@
|
|||||||
"page.settings.title": "Impostazioni",
|
"page.settings.title": "Impostazioni",
|
||||||
"page.settings.link_google_account": "Collega il mio account Google",
|
"page.settings.link_google_account": "Collega il mio account Google",
|
||||||
"page.settings.unlink_google_account": "Scollega il mio account Google",
|
"page.settings.unlink_google_account": "Scollega il mio account Google",
|
||||||
"page.settings.link_oidc_account": "Collega il mio account %s",
|
"page.settings.link_oidc_account": "Collega il mio account OpenID Connect",
|
||||||
"page.settings.unlink_oidc_account": "Scollega il mio account %s",
|
"page.settings.unlink_oidc_account": "Scollega il mio account OpenID Connect",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Actions",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||||
@ -226,7 +221,7 @@
|
|||||||
],
|
],
|
||||||
"page.login.title": "Accedi",
|
"page.login.title": "Accedi",
|
||||||
"page.login.google_signin": "Accedi tramite Google",
|
"page.login.google_signin": "Accedi tramite Google",
|
||||||
"page.login.oidc_signin": "Accedi tramite %s",
|
"page.login.oidc_signin": "Accedi tramite OpenID Connect",
|
||||||
"page.login.webauthn_login": "Accedi con passkey",
|
"page.login.webauthn_login": "Accedi con passkey",
|
||||||
"page.login.webauthn_login.error": "Impossibile accedere con passkey",
|
"page.login.webauthn_login.error": "Impossibile accedere con passkey",
|
||||||
"page.integrations.title": "Integrazioni",
|
"page.integrations.title": "Integrazioni",
|
||||||
@ -261,7 +256,6 @@
|
|||||||
"alert.no_bookmark": "Nessun preferito disponibile.",
|
"alert.no_bookmark": "Nessun preferito disponibile.",
|
||||||
"alert.no_category": "Nessuna categoria disponibile.",
|
"alert.no_category": "Nessuna categoria disponibile.",
|
||||||
"alert.no_category_entry": "Questa categoria non contiene alcun articolo.",
|
"alert.no_category_entry": "Questa categoria non contiene alcun articolo.",
|
||||||
"alert.no_tag_entry": "Non ci sono voci corrispondenti a questo tag.",
|
|
||||||
"alert.no_feed_entry": "Questo feed non contiene alcun articolo.",
|
"alert.no_feed_entry": "Questo feed non contiene alcun articolo.",
|
||||||
"alert.no_feed": "Nessun feed disponibile.",
|
"alert.no_feed": "Nessun feed disponibile.",
|
||||||
"alert.no_feed_in_category": "Non esiste un abbonamento per questa categoria.",
|
"alert.no_feed_in_category": "Non esiste un abbonamento per questa categoria.",
|
||||||
@ -296,14 +290,6 @@
|
|||||||
"error.password_min_length": "La password deve contenere almeno 6 caratteri.",
|
"error.password_min_length": "La password deve contenere almeno 6 caratteri.",
|
||||||
"error.settings_mandatory_fields": "Il nome utente, il tema, la lingua ed il fuso orario sono campi obbligatori.",
|
"error.settings_mandatory_fields": "Il nome utente, il tema, la lingua ed il fuso orario sono campi obbligatori.",
|
||||||
"error.settings_reading_speed_is_positive": "Le velocità di lettura devono essere numeri interi positivi.",
|
"error.settings_reading_speed_is_positive": "Le velocità di lettura devono essere numeri interi positivi.",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.entries_per_page_invalid": "Il numero di articoli per pagina non è valido.",
|
"error.entries_per_page_invalid": "Il numero di articoli per pagina non è valido.",
|
||||||
"error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.",
|
"error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.",
|
||||||
"error.feed_already_exists": "Questo feed esiste già.",
|
"error.feed_already_exists": "Questo feed esiste già.",
|
||||||
@ -328,7 +314,6 @@
|
|||||||
"form.feed.label.title": "Titolo",
|
"form.feed.label.title": "Titolo",
|
||||||
"form.feed.label.site_url": "URL del sito",
|
"form.feed.label.site_url": "URL del sito",
|
||||||
"form.feed.label.feed_url": "URL del feed",
|
"form.feed.label.feed_url": "URL del feed",
|
||||||
"form.feed.label.description": "Descrizione",
|
|
||||||
"form.feed.label.category": "Categoria",
|
"form.feed.label.category": "Categoria",
|
||||||
"form.feed.label.crawler": "Scarica il contenuto integrale",
|
"form.feed.label.crawler": "Scarica il contenuto integrale",
|
||||||
"form.feed.label.feed_username": "Nome utente del feed",
|
"form.feed.label.feed_username": "Nome utente del feed",
|
||||||
@ -348,13 +333,6 @@
|
|||||||
"form.feed.label.disabled": "Non aggiornare questo feed",
|
"form.feed.label.disabled": "Non aggiornare questo feed",
|
||||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||||
"form.feed.label.hide_globally": "Nascondere le voci nella lista globale dei non letti",
|
"form.feed.label.hide_globally": "Nascondere le voci nella lista globale dei non letti",
|
||||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
|
||||||
"form.feed.fieldset.general": "General",
|
"form.feed.fieldset.general": "General",
|
||||||
"form.feed.fieldset.rules": "Rules",
|
"form.feed.fieldset.rules": "Rules",
|
||||||
"form.feed.fieldset.network_settings": "Network Settings",
|
"form.feed.fieldset.network_settings": "Network Settings",
|
||||||
@ -395,18 +373,11 @@
|
|||||||
"form.prefs.label.default_home_page": "Pagina iniziale predefinita",
|
"form.prefs.label.default_home_page": "Pagina iniziale predefinita",
|
||||||
"form.prefs.label.categories_sorting_order": "Ordinamento delle categorie",
|
"form.prefs.label.categories_sorting_order": "Ordinamento delle categorie",
|
||||||
"form.prefs.label.mark_read_on_view": "Contrassegna automaticamente le voci come lette quando visualizzate",
|
"form.prefs.label.mark_read_on_view": "Contrassegna automaticamente le voci come lette quando visualizzate",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
|
||||||
"form.import.label.file": "File OPML",
|
"form.import.label.file": "File OPML",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "Save entries to Betula",
|
|
||||||
"form.integration.betula_url": "Betula server URL",
|
|
||||||
"form.integration.betula_token": "Betula Token",
|
|
||||||
"form.integration.fever_activate": "Abilita l'API di Fever",
|
"form.integration.fever_activate": "Abilita l'API di Fever",
|
||||||
"form.integration.fever_username": "Nome utente dell'account Fever",
|
"form.integration.fever_username": "Nome utente dell'account Fever",
|
||||||
"form.integration.fever_password": "Password dell'account Fever",
|
"form.integration.fever_password": "Password dell'account Fever",
|
||||||
@ -478,10 +449,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Password per l'utente Matrix",
|
"form.integration.matrix_bot_password": "Password per l'utente Matrix",
|
||||||
"form.integration.matrix_bot_url": "URL del server Matrix",
|
"form.integration.matrix_bot_url": "URL del server Matrix",
|
||||||
"form.integration.matrix_bot_chat_id": "ID della stanza Matrix",
|
"form.integration.matrix_bot_chat_id": "ID della stanza Matrix",
|
||||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "Salva gli articoli su Readeck",
|
"form.integration.readeck_activate": "Salva gli articoli su Readeck",
|
||||||
"form.integration.readeck_endpoint": "Endpoint dell'API di Readeck",
|
"form.integration.readeck_endpoint": "Endpoint dell'API di Readeck",
|
||||||
"form.integration.readeck_api_key": "API key dell'account Readeck",
|
"form.integration.readeck_api_key": "API key dell'account Readeck",
|
||||||
@ -500,13 +467,6 @@
|
|||||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.api_key.label.description": "Etichetta chiave API",
|
"form.api_key.label.description": "Etichetta chiave API",
|
||||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
|
||||||
"form.submit.loading": "Caricamento in corso...",
|
"form.submit.loading": "Caricamento in corso...",
|
||||||
"form.submit.saving": "Salvataggio in corso...",
|
"form.submit.saving": "Salvataggio in corso...",
|
||||||
"time_elapsed.not_yet": "non ancora",
|
"time_elapsed.not_yet": "non ancora",
|
||||||
@ -545,7 +505,7 @@
|
|||||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP client error: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
@ -564,16 +524,5 @@
|
|||||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "Velocità di riproduzione dell'audio/video",
|
|
||||||
"error.settings_media_playback_rate_range": "La velocità di riproduzione non rientra nell'intervallo",
|
|
||||||
"enclosure_media_controls.seek" : "Seek:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Speed:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Faster",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Slower",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "すべて既読にする",
|
"menu.mark_all_as_read": "すべて既読にする",
|
||||||
"menu.show_all_entries": "すべての記事を表示",
|
"menu.show_all_entries": "すべての記事を表示",
|
||||||
"menu.show_only_unread_entries": "未読の記事だけを表示",
|
"menu.show_only_unread_entries": "未読の記事だけを表示",
|
||||||
"menu.show_only_starred_entries": "Show only starred entries",
|
|
||||||
"menu.refresh_feed": "更新",
|
"menu.refresh_feed": "更新",
|
||||||
"menu.refresh_all_feeds": "すべてのフィードをバックグラウンドで更新",
|
"menu.refresh_all_feeds": "すべてのフィードをバックグラウンドで更新",
|
||||||
"menu.edit_feed": "編集",
|
"menu.edit_feed": "編集",
|
||||||
@ -56,9 +55,7 @@
|
|||||||
"search.label": "検索",
|
"search.label": "検索",
|
||||||
"search.placeholder": "…を検索",
|
"search.placeholder": "…を検索",
|
||||||
"search.submit": "Search",
|
"search.submit": "Search",
|
||||||
"pagination.last": "Last",
|
|
||||||
"pagination.next": "次",
|
"pagination.next": "次",
|
||||||
"pagination.first": "First",
|
|
||||||
"pagination.previous": "前",
|
"pagination.previous": "前",
|
||||||
"entry.status.unread": "未読にする",
|
"entry.status.unread": "未読にする",
|
||||||
"entry.status.read": "既読にする",
|
"entry.status.read": "既読にする",
|
||||||
@ -172,8 +169,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "フィード",
|
"page.keyboard_shortcuts.go_to_feed": "フィード",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "前のページ",
|
"page.keyboard_shortcuts.go_to_previous_page": "前のページ",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "次のページ",
|
"page.keyboard_shortcuts.go_to_next_page": "次のページ",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "一番下の項目に移動",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "先頭の項目に移動",
|
|
||||||
"page.keyboard_shortcuts.open_item": "選択されたアイテムを開く",
|
"page.keyboard_shortcuts.open_item": "選択されたアイテムを開く",
|
||||||
"page.keyboard_shortcuts.open_original": "オリジナルのリンクを開く",
|
"page.keyboard_shortcuts.open_original": "オリジナルのリンクを開く",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "現在のタブでオリジナルのリンクを開く",
|
"page.keyboard_shortcuts.open_original_same_window": "現在のタブでオリジナルのリンクを開く",
|
||||||
@ -202,8 +197,8 @@
|
|||||||
"page.settings.title": "設定",
|
"page.settings.title": "設定",
|
||||||
"page.settings.link_google_account": "Google アカウントと接続する",
|
"page.settings.link_google_account": "Google アカウントと接続する",
|
||||||
"page.settings.unlink_google_account": "Google アカウントと接続を解除する",
|
"page.settings.unlink_google_account": "Google アカウントと接続を解除する",
|
||||||
"page.settings.link_oidc_account": "%s アカウントと接続する",
|
"page.settings.link_oidc_account": "OpenID Connect アカウントと接続する",
|
||||||
"page.settings.unlink_oidc_account": "%s アカウントと接続を解除する",
|
"page.settings.unlink_oidc_account": "OpenID Connect アカウントと接続を解除する",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Actions",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||||
@ -216,7 +211,7 @@
|
|||||||
],
|
],
|
||||||
"page.login.title": "ログイン",
|
"page.login.title": "ログイン",
|
||||||
"page.login.google_signin": "Google アカウントでログイン",
|
"page.login.google_signin": "Google アカウントでログイン",
|
||||||
"page.login.oidc_signin": "%s アカウントでログイン",
|
"page.login.oidc_signin": "OpenID Connect アカウントでログイン",
|
||||||
"page.login.webauthn_login": "パスキーでログイン",
|
"page.login.webauthn_login": "パスキーでログイン",
|
||||||
"page.login.webauthn_login.error": "パスキーでログインできない",
|
"page.login.webauthn_login.error": "パスキーでログインできない",
|
||||||
"page.integrations.title": "連携",
|
"page.integrations.title": "連携",
|
||||||
@ -251,7 +246,6 @@
|
|||||||
"alert.no_bookmark": "現在星付きはありません。",
|
"alert.no_bookmark": "現在星付きはありません。",
|
||||||
"alert.no_category": "カテゴリが存在しません。",
|
"alert.no_category": "カテゴリが存在しません。",
|
||||||
"alert.no_category_entry": "このカテゴリには記事がありません。",
|
"alert.no_category_entry": "このカテゴリには記事がありません。",
|
||||||
"alert.no_tag_entry": "このタグに一致するエントリーはありません。",
|
|
||||||
"alert.no_feed_entry": "このフィードには記事がありません。",
|
"alert.no_feed_entry": "このフィードには記事がありません。",
|
||||||
"alert.no_feed": "何も購読していません。",
|
"alert.no_feed": "何も購読していません。",
|
||||||
"alert.no_feed_in_category": "このカテゴリには購読中のフィードがありません。",
|
"alert.no_feed_in_category": "このカテゴリには購読中のフィードがありません。",
|
||||||
@ -293,14 +287,6 @@
|
|||||||
"error.password_min_length": "パスワードは6文字以上である必要があります。",
|
"error.password_min_length": "パスワードは6文字以上である必要があります。",
|
||||||
"error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンのすべてが必要です。",
|
"error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンのすべてが必要です。",
|
||||||
"error.settings_reading_speed_is_positive": "読書速度は正の整数である必要があります。",
|
"error.settings_reading_speed_is_positive": "読書速度は正の整数である必要があります。",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.entries_per_page_invalid": "ページあたりの記事数が無効です。",
|
"error.entries_per_page_invalid": "ページあたりの記事数が無効です。",
|
||||||
"error.feed_mandatory_fields": "URL と カテゴリが必要です。",
|
"error.feed_mandatory_fields": "URL と カテゴリが必要です。",
|
||||||
"error.feed_already_exists": "このフィードは既に存在します。",
|
"error.feed_already_exists": "このフィードは既に存在します。",
|
||||||
@ -318,7 +304,6 @@
|
|||||||
"form.feed.label.title": "タイトル",
|
"form.feed.label.title": "タイトル",
|
||||||
"form.feed.label.site_url": "サイト URL",
|
"form.feed.label.site_url": "サイト URL",
|
||||||
"form.feed.label.feed_url": "フィード URL",
|
"form.feed.label.feed_url": "フィード URL",
|
||||||
"form.feed.label.description": "説明",
|
|
||||||
"form.feed.label.category": "カテゴリ",
|
"form.feed.label.category": "カテゴリ",
|
||||||
"form.feed.label.crawler": "オリジナルの内容を取得",
|
"form.feed.label.crawler": "オリジナルの内容を取得",
|
||||||
"form.feed.label.feed_username": "フィードのユーザー名",
|
"form.feed.label.feed_username": "フィードのユーザー名",
|
||||||
@ -338,13 +323,6 @@
|
|||||||
"form.feed.label.disabled": "このフィードを更新しない",
|
"form.feed.label.disabled": "このフィードを更新しない",
|
||||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||||
"form.feed.label.hide_globally": "未読一覧に記事を表示しない",
|
"form.feed.label.hide_globally": "未読一覧に記事を表示しない",
|
||||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
|
||||||
"form.feed.fieldset.general": "General",
|
"form.feed.fieldset.general": "General",
|
||||||
"form.feed.fieldset.rules": "Rules",
|
"form.feed.fieldset.rules": "Rules",
|
||||||
"form.feed.fieldset.network_settings": "Network Settings",
|
"form.feed.fieldset.network_settings": "Network Settings",
|
||||||
@ -385,18 +363,11 @@
|
|||||||
"form.prefs.label.default_home_page": "デフォルトのトップページ",
|
"form.prefs.label.default_home_page": "デフォルトのトップページ",
|
||||||
"form.prefs.label.categories_sorting_order": "カテゴリの表示順",
|
"form.prefs.label.categories_sorting_order": "カテゴリの表示順",
|
||||||
"form.prefs.label.mark_read_on_view": "表示時にエントリを自動的に既読としてマークします",
|
"form.prefs.label.mark_read_on_view": "表示時にエントリを自動的に既読としてマークします",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
|
||||||
"form.import.label.file": "OPML ファイル",
|
"form.import.label.file": "OPML ファイル",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "Save entries to Betula",
|
|
||||||
"form.integration.betula_url": "Betula server URL",
|
|
||||||
"form.integration.betula_token": "Betula Token",
|
|
||||||
"form.integration.fever_activate": "Fever API を有効にする",
|
"form.integration.fever_activate": "Fever API を有効にする",
|
||||||
"form.integration.fever_username": "Fever のユーザー名",
|
"form.integration.fever_username": "Fever のユーザー名",
|
||||||
"form.integration.fever_password": "Fever のパスワード",
|
"form.integration.fever_password": "Fever のパスワード",
|
||||||
@ -468,10 +439,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Matrixユーザ用パスワード",
|
"form.integration.matrix_bot_password": "Matrixユーザ用パスワード",
|
||||||
"form.integration.matrix_bot_url": "MatrixサーバーのURL",
|
"form.integration.matrix_bot_url": "MatrixサーバーのURL",
|
||||||
"form.integration.matrix_bot_chat_id": "MatrixルームのID",
|
"form.integration.matrix_bot_chat_id": "MatrixルームのID",
|
||||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "Readeck に記事を保存する",
|
"form.integration.readeck_activate": "Readeck に記事を保存する",
|
||||||
"form.integration.readeck_endpoint": "Readeck の API Endpoint",
|
"form.integration.readeck_endpoint": "Readeck の API Endpoint",
|
||||||
"form.integration.readeck_api_key": "Readeck の API key",
|
"form.integration.readeck_api_key": "Readeck の API key",
|
||||||
@ -489,13 +456,6 @@
|
|||||||
"form.integration.webhook_secret": "Webhook Secret",
|
"form.integration.webhook_secret": "Webhook Secret",
|
||||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
|
||||||
"form.api_key.label.description": "API キーラベル",
|
"form.api_key.label.description": "API キーラベル",
|
||||||
"form.submit.loading": "読み込み中…",
|
"form.submit.loading": "読み込み中…",
|
||||||
"form.submit.saving": "保存中…",
|
"form.submit.saving": "保存中…",
|
||||||
@ -528,7 +488,7 @@
|
|||||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP client error: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
@ -547,16 +507,5 @@
|
|||||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "オーディオ/ビデオの再生速度",
|
|
||||||
"error.settings_media_playback_rate_range": "再生速度が範囲外",
|
|
||||||
"enclosure_media_controls.seek" : "Seek:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Speed:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Faster",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Slower",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"skip_to_content": "Ga naar inhoud",
|
"skip_to_content": "Skip to content",
|
||||||
"confirm.question": "Weet je het zeker?",
|
"confirm.question": "Weet je het zeker?",
|
||||||
"confirm.question.refresh": "Wil je vernieuwen forceren?",
|
"confirm.question.refresh": "Wil je een gedwongen vernieuwing uitvoeren?",
|
||||||
"confirm.yes": "ja",
|
"confirm.yes": "ja",
|
||||||
"confirm.no": "nee",
|
"confirm.no": "nee",
|
||||||
"confirm.loading": "Bezig...",
|
"confirm.loading": "Bezig...",
|
||||||
"action.subscribe": "Abonneren",
|
"action.subscribe": "Abboneren",
|
||||||
"action.save": "Opslaan",
|
"action.save": "Opslaan",
|
||||||
"action.or": "of",
|
"action.or": "of",
|
||||||
"action.cancel": "annuleren",
|
"action.cancel": "annuleren",
|
||||||
"action.remove": "Verwijderen",
|
"action.remove": "Verwijderen",
|
||||||
"action.remove_feed": "Verwijder deze feed",
|
"action.remove_feed": "Verwijder deze feed",
|
||||||
"action.update": "Bijwerken",
|
"action.update": "Updaten",
|
||||||
"action.edit": "Bewerken",
|
"action.edit": "Bewerken",
|
||||||
"action.download": "Download",
|
"action.download": "Download",
|
||||||
"action.import": "Importeren",
|
"action.import": "Importeren",
|
||||||
@ -20,7 +20,7 @@
|
|||||||
"tooltip.keyboard_shortcuts": "Sneltoets: %s",
|
"tooltip.keyboard_shortcuts": "Sneltoets: %s",
|
||||||
"tooltip.logged_user": "Ingelogd als %s",
|
"tooltip.logged_user": "Ingelogd als %s",
|
||||||
"menu.title": "Menu",
|
"menu.title": "Menu",
|
||||||
"menu.home_page": "Startpagina",
|
"menu.home_page": "Home page",
|
||||||
"menu.unread": "Ongelezen",
|
"menu.unread": "Ongelezen",
|
||||||
"menu.starred": "Favorieten",
|
"menu.starred": "Favorieten",
|
||||||
"menu.history": "Geschiedenis",
|
"menu.history": "Geschiedenis",
|
||||||
@ -31,17 +31,16 @@
|
|||||||
"menu.preferences": "Voorkeuren",
|
"menu.preferences": "Voorkeuren",
|
||||||
"menu.integrations": "Integraties",
|
"menu.integrations": "Integraties",
|
||||||
"menu.sessions": "Sessies",
|
"menu.sessions": "Sessies",
|
||||||
"menu.users": "Gebruikers",
|
"menu.users": "Users",
|
||||||
"menu.about": "Over",
|
"menu.about": "Over",
|
||||||
"menu.export": "Exporteren",
|
"menu.export": "Exporteren",
|
||||||
"menu.import": "Importeren",
|
"menu.import": "Importeren",
|
||||||
"menu.search": "Zoeken",
|
"menu.search": "Zoeken",
|
||||||
"menu.create_category": "Categorie toevoegen",
|
"menu.create_category": "Categorie toevoegen",
|
||||||
"menu.mark_page_as_read": "Markeer deze pagina als gelezen",
|
"menu.mark_page_as_read": "Markeer deze pagina als gelezen",
|
||||||
"menu.mark_all_as_read": "Markeer alles als gelezen",
|
"menu.mark_all_as_read": "Markeer alle items als gelezen",
|
||||||
"menu.show_all_entries": "Toon alle artikelen",
|
"menu.show_all_entries": "Toon alle artikelen",
|
||||||
"menu.show_only_unread_entries": "Toon alleen ongelezen artikelen",
|
"menu.show_only_unread_entries": "Toon alleen ongelezen artikelen",
|
||||||
"menu.show_only_starred_entries": "Toon alleen favorieten",
|
|
||||||
"menu.refresh_feed": "Vernieuwen",
|
"menu.refresh_feed": "Vernieuwen",
|
||||||
"menu.refresh_all_feeds": "Vernieuw alle feeds in de achtergrond",
|
"menu.refresh_all_feeds": "Vernieuw alle feeds in de achtergrond",
|
||||||
"menu.edit_feed": "Bewerken",
|
"menu.edit_feed": "Bewerken",
|
||||||
@ -49,39 +48,37 @@
|
|||||||
"menu.add_feed": "Feed toevoegen",
|
"menu.add_feed": "Feed toevoegen",
|
||||||
"menu.add_user": "Gebruiker toevoegen",
|
"menu.add_user": "Gebruiker toevoegen",
|
||||||
"menu.flush_history": "Verwijder geschiedenis",
|
"menu.flush_history": "Verwijder geschiedenis",
|
||||||
"menu.feed_entries": "Artikelen",
|
"menu.feed_entries": "Lidwoord",
|
||||||
"menu.api_keys": "API-sleutels",
|
"menu.api_keys": "API-sleutels",
|
||||||
"menu.create_api_key": "Maak een nieuwe API-sleutel",
|
"menu.create_api_key": "Maak een nieuwe API-sleutel",
|
||||||
"menu.shared_entries": "Gedeelde artikelen",
|
"menu.shared_entries": "Gedeelde vermeldingen",
|
||||||
"search.label": "Zoeken",
|
"search.label": "Zoeken",
|
||||||
"search.placeholder": "Zoeken...",
|
"search.placeholder": "Zoeken...",
|
||||||
"search.submit": "Zoeken",
|
"search.submit": "Search",
|
||||||
"pagination.last": "Laatste",
|
|
||||||
"pagination.next": "Volgende",
|
"pagination.next": "Volgende",
|
||||||
"pagination.first": "Eerste",
|
|
||||||
"pagination.previous": "Vorige",
|
"pagination.previous": "Vorige",
|
||||||
"entry.status.unread": "Ongelezen",
|
"entry.status.unread": "Ongelezen",
|
||||||
"entry.status.read": "Gelezen",
|
"entry.status.read": "Gelezen",
|
||||||
"entry.status.toast.unread": "Gemarkeerd als ongelezen",
|
"entry.status.toast.unread": "Gemarkeerd als ongelezen",
|
||||||
"entry.status.toast.read": "Gemarkeerd als gelezen",
|
"entry.status.toast.read": "Gemarkeerd als gelezen",
|
||||||
"entry.status.title": "Verander artikelstatus",
|
"entry.status.title": "Verander status van item",
|
||||||
"entry.bookmark.toggle.on": "Favoriet",
|
"entry.bookmark.toggle.on": "Ster toevoegen",
|
||||||
"entry.bookmark.toggle.off": "Favoriet verwijderen",
|
"entry.bookmark.toggle.off": "Ster weghalen",
|
||||||
"entry.bookmark.toast.on": "Favoriet toegevoegd",
|
"entry.bookmark.toast.on": "Met ster",
|
||||||
"entry.bookmark.toast.off": "Favoriet verwijderd",
|
"entry.bookmark.toast.off": "Ster verwijderd",
|
||||||
"entry.state.saving": "Opslaan...",
|
"entry.state.saving": "Opslaag...",
|
||||||
"entry.state.loading": "Laden...",
|
"entry.state.loading": "Laden...",
|
||||||
"entry.save.label": "Opslaan",
|
"entry.save.label": "Opslaan",
|
||||||
"entry.save.title": "Artikel opslaan",
|
"entry.save.title": "Artikel opslaan",
|
||||||
"entry.save.completed": "Klaar!",
|
"entry.save.completed": "Done!",
|
||||||
"entry.save.toast.completed": "Artikel opgeslagen",
|
"entry.save.toast.completed": "Artikel opgeslagen",
|
||||||
"entry.scraper.label": "Downloaden",
|
"entry.scraper.label": "Downloaden",
|
||||||
"entry.scraper.title": "Originele inhoud ophalen",
|
"entry.scraper.title": "Fetch original content",
|
||||||
"entry.scraper.completed": "Klaar!",
|
"entry.scraper.completed": "Klaar!",
|
||||||
"entry.external_link.label": "Externe link",
|
"entry.external_link.label": "Externe link",
|
||||||
"entry.comments.label": "Reacties",
|
"entry.comments.label": "Comments",
|
||||||
"entry.comments.title": "Bekijk reacties",
|
"entry.comments.title": "Bekijk de reacties",
|
||||||
"entry.share.label": "Delen",
|
"entry.share.label": "Deel",
|
||||||
"entry.share.title": "Deel dit artikel",
|
"entry.share.title": "Deel dit artikel",
|
||||||
"entry.unshare.label": "Delen ongedaan maken",
|
"entry.unshare.label": "Delen ongedaan maken",
|
||||||
"entry.shared_entry.title": "Open de openbare link",
|
"entry.shared_entry.title": "Open de openbare link",
|
||||||
@ -90,117 +87,115 @@
|
|||||||
"%d minuut leestijd",
|
"%d minuut leestijd",
|
||||||
"%d minuten leestijd"
|
"%d minuten leestijd"
|
||||||
],
|
],
|
||||||
"entry.tags.label": "Tags:",
|
"entry.tags.label": "Labels:",
|
||||||
"page.shared_entries.title": "Gedeelde artikelen",
|
"page.shared_entries.title": "Gedeelde vermeldingen",
|
||||||
"page.shared_entries_count": [
|
"page.shared_entries_count": [
|
||||||
"%d gedeeld artikel",
|
"%d shared entry",
|
||||||
"%d gedeelde artikelen"
|
"%d shared entries"
|
||||||
],
|
],
|
||||||
"page.unread.title": "Ongelezen",
|
"page.unread.title": "Ongelezen",
|
||||||
"page.unread_entry_count": [
|
"page.unread_entry_count": [
|
||||||
"%d ongelezen artikel",
|
"%d unread entry",
|
||||||
"%d ongelezen artikelen"
|
"%d unread entries"
|
||||||
],
|
],
|
||||||
"page.total_entry_count": [
|
"page.total_entry_count": [
|
||||||
"%d artikel totaal",
|
"%d entry in total",
|
||||||
"%d artikelen totaal"
|
"%d entries in total"
|
||||||
],
|
],
|
||||||
"page.starred.title": "Favorieten",
|
"page.starred.title": "Favorieten",
|
||||||
"page.starred_entry_count": [
|
"page.starred_entry_count": [
|
||||||
"%d favoriet artikel",
|
"%d starred entry",
|
||||||
"%d favoriete artikelen"
|
"%d starred entries"
|
||||||
],
|
],
|
||||||
"page.categories.title": "Categorieën",
|
"page.categories.title": "Categorieën",
|
||||||
"page.categories.no_feed": "Geen feed.",
|
"page.categories.no_feed": "Geen feeds.",
|
||||||
"page.categories.entries": "Artikelen",
|
"page.categories.entries": "Lidwoord",
|
||||||
"page.categories.feeds": "Feeds",
|
"page.categories.feeds": "Abonnementen",
|
||||||
"page.categories.feed_count": [
|
"page.categories.feed_count": [
|
||||||
"Er is %d feed.",
|
"Er is %d feed.",
|
||||||
"Er zijn %d feeds."
|
"Er zijn %d feeds."
|
||||||
],
|
],
|
||||||
"page.categories_count": [
|
"page.categories_count": [
|
||||||
"%d categorie",
|
"%d category",
|
||||||
"%d categorieën"
|
"%d categories"
|
||||||
],
|
],
|
||||||
"page.new_category.title": "Nieuwe categorie",
|
"page.new_category.title": "Nieuwe categorie",
|
||||||
"page.new_user.title": "Nieuwe gebruiker",
|
"page.new_user.title": "Nieuwe gebruiker",
|
||||||
"page.edit_category.title": "Bewerk categorie: %s",
|
"page.edit_category.title": "Bewerken van categorie: %s",
|
||||||
"page.edit_user.title": "Bewerk gebruiker: %s",
|
"page.edit_user.title": "Bewerk gebruiker: %s",
|
||||||
"page.feeds.title": "Feeds",
|
"page.feeds.title": "Feeds",
|
||||||
"page.category_label": "Categorie: %s",
|
"page.category_label": "Category: %s",
|
||||||
"page.feeds.last_check": "Laatste controle:",
|
"page.feeds.last_check": "Laatste update:",
|
||||||
"page.feeds.next_check": "Volgende controle:",
|
"page.feeds.next_check": "Next check:",
|
||||||
"page.feeds.read_counter": "Aantal gelezen artikelen",
|
"page.feeds.read_counter": "Aantal gelezen vermeldingen",
|
||||||
"page.feeds.error_count": [
|
"page.feeds.error_count": [
|
||||||
"%d fout",
|
"%d error",
|
||||||
"%d fouten"
|
"%d errors"
|
||||||
],
|
],
|
||||||
"page.history.title": "Geschiedenis",
|
"page.history.title": "Geschiedenis",
|
||||||
"page.read_entry_count": [
|
"page.read_entry_count": [
|
||||||
"%d gelezen artikel",
|
"%d read entry",
|
||||||
"%d gelezen artikelen"
|
"%d read entries"
|
||||||
],
|
],
|
||||||
"page.import.title": "Importeren",
|
"page.import.title": "Importeren",
|
||||||
"page.login.title": "Inloggen",
|
"page.login.title": "Inloggen",
|
||||||
"page.search.title": "Zoekresultaten",
|
"page.search.title": "Zoekresultaten",
|
||||||
"page.about.title": "Over",
|
"page.about.title": "Over",
|
||||||
"page.about.credits": "Credits",
|
"page.about.credits": "Copyrights",
|
||||||
"page.about.version": "Versie:",
|
"page.about.version": "Versie:",
|
||||||
"page.about.build_date": "Compilatiedatum:",
|
"page.about.build_date": "Datum build:",
|
||||||
"page.about.author": "Auteur:",
|
"page.about.author": "Auteur:",
|
||||||
"page.about.license": "Licentie:",
|
"page.about.license": "Licentie:",
|
||||||
"page.about.global_config_options": "Globale Configuratie Opties",
|
"page.about.global_config_options": "globale configuratie-opties",
|
||||||
"page.about.postgres_version": "Postgres versie:",
|
"page.about.postgres_version": "Postgres versie:",
|
||||||
"page.about.go_version": "Go versie:",
|
"page.about.go_version": "Go versie:",
|
||||||
"page.add_feed.title": "Nieuwe feed",
|
"page.add_feed.title": "Nieuwe feed",
|
||||||
"page.add_feed.no_category": "Er is geen categorie. Je moet minstens één categorie hebben.",
|
"page.add_feed.no_category": "Er zijn geen categorieën. Je moet op zijn minst één caterogie hebben.",
|
||||||
"page.add_feed.label.url": "URL",
|
"page.add_feed.label.url": "URL",
|
||||||
"page.add_feed.submit": "Feed zoeken",
|
"page.add_feed.submit": "Feed zoeken",
|
||||||
"page.add_feed.legend.advanced_options": "Geavanceerde opties",
|
"page.add_feed.legend.advanced_options": "Geavanceerde mogelijkheden",
|
||||||
"page.add_feed.choose_feed": "Feed kiezen",
|
"page.add_feed.choose_feed": "Feed kiezen",
|
||||||
"page.edit_feed.title": "Bewerk feed: %s",
|
"page.edit_feed.title": "Bewerken van feed: %s",
|
||||||
"page.edit_feed.last_check": "Laatste controle:",
|
"page.edit_feed.last_check": "Laatste update:",
|
||||||
"page.edit_feed.last_modified_header": "LastModified header:",
|
"page.edit_feed.last_modified_header": "LastModified-header:",
|
||||||
"page.edit_feed.etag_header": "ETAG header:",
|
"page.edit_feed.etag_header": "ETAG-header:",
|
||||||
"page.edit_feed.no_header": "Geen",
|
"page.edit_feed.no_header": "Geen",
|
||||||
"page.edit_feed.last_parsing_error": "Laatste analysefout",
|
"page.edit_feed.last_parsing_error": "Laatste parse error",
|
||||||
"page.entry.attachments": "Bijlagen",
|
"page.entry.attachments": "Bijlagen",
|
||||||
"page.keyboard_shortcuts.title": "Sneltoetsen",
|
"page.keyboard_shortcuts.title": "Sneltoetsen",
|
||||||
"page.keyboard_shortcuts.subtitle.sections": "Navigeren door menu's",
|
"page.keyboard_shortcuts.subtitle.sections": "Naviguatie tussen menu's",
|
||||||
"page.keyboard_shortcuts.subtitle.items": "Navigeren door artikelen",
|
"page.keyboard_shortcuts.subtitle.items": "Navigatie tussen items",
|
||||||
"page.keyboard_shortcuts.subtitle.pages": "Navigeren door pagina's",
|
"page.keyboard_shortcuts.subtitle.pages": "Naviguatie tussen pagina's",
|
||||||
"page.keyboard_shortcuts.subtitle.actions": "Acties",
|
"page.keyboard_shortcuts.subtitle.actions": "Actions",
|
||||||
"page.keyboard_shortcuts.go_to_unread": "Ga naar ongelezen",
|
"page.keyboard_shortcuts.go_to_unread": "Ga naar ongelezen",
|
||||||
"page.keyboard_shortcuts.go_to_starred": "Ga naar favorieten",
|
"page.keyboard_shortcuts.go_to_starred": "Ga naar favorieten",
|
||||||
"page.keyboard_shortcuts.go_to_history": "Ga naar geschiedenis",
|
"page.keyboard_shortcuts.go_to_history": "Ga naar geschiedenis",
|
||||||
"page.keyboard_shortcuts.go_to_feeds": "Ga naar feeds",
|
"page.keyboard_shortcuts.go_to_feeds": "Ga naar feeds",
|
||||||
"page.keyboard_shortcuts.go_to_categories": "Ga naar categorieën",
|
"page.keyboard_shortcuts.go_to_categories": "Ga naar categorieën",
|
||||||
"page.keyboard_shortcuts.go_to_settings": "Ga naar instellingen",
|
"page.keyboard_shortcuts.go_to_settings": "Ga naar instellingen",
|
||||||
"page.keyboard_shortcuts.show_keyboard_shortcuts": "Sneltoetsen tonen",
|
"page.keyboard_shortcuts.show_keyboard_shortcuts": "Laat sneltoetsen zien",
|
||||||
"page.keyboard_shortcuts.go_to_previous_item": "Vorig artikel",
|
"page.keyboard_shortcuts.go_to_previous_item": "Vorige item",
|
||||||
"page.keyboard_shortcuts.go_to_next_item": "Volgend artikel",
|
"page.keyboard_shortcuts.go_to_next_item": "Volgende item",
|
||||||
"page.keyboard_shortcuts.go_to_feed": "Ga naar feed",
|
"page.keyboard_shortcuts.go_to_feed": "Ga naar feed",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "Vorige pagina",
|
"page.keyboard_shortcuts.go_to_previous_page": "Vorige pagina",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "Volgende pagina",
|
"page.keyboard_shortcuts.go_to_next_page": "Volgende pagina",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "Ga naar het onderste artikel",
|
"page.keyboard_shortcuts.open_item": "Open geselecteerde link",
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "Ga naar het bovenste artikel",
|
|
||||||
"page.keyboard_shortcuts.open_item": "Open geselecteerd artikel",
|
|
||||||
"page.keyboard_shortcuts.open_original": "Open originele link",
|
"page.keyboard_shortcuts.open_original": "Open originele link",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "Open originele link in huidig tabblad",
|
"page.keyboard_shortcuts.open_original_same_window": "Oorspronkelijke koppeling op huidig tabblad openen",
|
||||||
"page.keyboard_shortcuts.open_comments": "Open reacties",
|
"page.keyboard_shortcuts.open_comments": "Open opmerkingen link",
|
||||||
"page.keyboard_shortcuts.open_comments_same_window": "Open reacties in huidig tabblad",
|
"page.keyboard_shortcuts.open_comments_same_window": "Open de reactiekoppeling op het huidige tabblad",
|
||||||
"page.keyboard_shortcuts.toggle_read_status_next": "Markeer gelezen/ongelezen, focus volgende",
|
"page.keyboard_shortcuts.toggle_read_status_next": "Markeer gelezen/ongelezen, focus volgende",
|
||||||
"page.keyboard_shortcuts.toggle_read_status_prev": "Markeer gelezen/ongelezen, focus vorige",
|
"page.keyboard_shortcuts.toggle_read_status_prev": "Markeer gelezen/ongelezen, focus vorige",
|
||||||
"page.keyboard_shortcuts.refresh_all_feeds": "Vernieuw alle feeds in de achtergrond",
|
"page.keyboard_shortcuts.refresh_all_feeds": "Vernieuw alle feeds in de achtergrond",
|
||||||
"page.keyboard_shortcuts.mark_page_as_read": "Markeer huidige pagina als gelezen",
|
"page.keyboard_shortcuts.mark_page_as_read": "Markeer deze pagina als gelezen",
|
||||||
"page.keyboard_shortcuts.download_content": "Download originele inhoud",
|
"page.keyboard_shortcuts.download_content": "Download originele content",
|
||||||
"page.keyboard_shortcuts.toggle_bookmark_status": "Favoriet toevoegen/verwijderen",
|
"page.keyboard_shortcuts.toggle_bookmark_status": "Ster toevoegen/weghalen",
|
||||||
"page.keyboard_shortcuts.save_article": "Artikel opslaan",
|
"page.keyboard_shortcuts.save_article": "Artikel opslaan",
|
||||||
"page.keyboard_shortcuts.scroll_item_to_top": "Scroll artikel naar boven",
|
"page.keyboard_shortcuts.scroll_item_to_top": "Scroll artikel naar boven",
|
||||||
"page.keyboard_shortcuts.remove_feed": "Verwijder deze feed",
|
"page.keyboard_shortcuts.remove_feed": "Verwijder deze feed",
|
||||||
"page.keyboard_shortcuts.go_to_search": "Focus instellen op zoekformulier",
|
"page.keyboard_shortcuts.go_to_search": "Focus instellen op zoekformulier",
|
||||||
"page.keyboard_shortcuts.toggle_entry_attachments": "Bijlagen van artikel openen/sluiten",
|
"page.keyboard_shortcuts.toggle_entry_attachments": "Toggle open/close entry attachments",
|
||||||
"page.keyboard_shortcuts.close_modal": "Dialoogvenster sluiten",
|
"page.keyboard_shortcuts.close_modal": "Sluit dialoogscherm",
|
||||||
"page.users.title": "Gebruikers",
|
"page.users.title": "Gebruikers",
|
||||||
"page.users.username": "Gebruikersnaam",
|
"page.users.username": "Gebruikersnaam",
|
||||||
"page.users.never_logged": "Nooit",
|
"page.users.never_logged": "Nooit",
|
||||||
@ -208,27 +203,27 @@
|
|||||||
"page.users.admin.no": "Nee",
|
"page.users.admin.no": "Nee",
|
||||||
"page.users.actions": "Acties",
|
"page.users.actions": "Acties",
|
||||||
"page.users.last_login": "Laatste login",
|
"page.users.last_login": "Laatste login",
|
||||||
"page.users.is_admin": "Beheerder",
|
"page.users.is_admin": "Administrator",
|
||||||
"page.settings.title": "Instellingen",
|
"page.settings.title": "Instellingen",
|
||||||
"page.settings.link_google_account": "Koppel mijn Google-account",
|
"page.settings.link_google_account": "Koppel mijn Google-account",
|
||||||
"page.settings.unlink_google_account": "Ontkoppel mijn Google-account",
|
"page.settings.unlink_google_account": "Ontkoppel mijn Google-account",
|
||||||
"page.settings.link_oidc_account": "Koppel mijn %s account",
|
"page.settings.link_oidc_account": "Koppel mijn OpenID Connect-account",
|
||||||
"page.settings.unlink_oidc_account": "Ontkoppel mijn %s account",
|
"page.settings.unlink_oidc_account": "Ontkoppel mijn OpenID Connect-account",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Acties",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey Naam",
|
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||||
"page.settings.webauthn.added_on": "Toegevoegd op",
|
"page.settings.webauthn.added_on": "Added On",
|
||||||
"page.settings.webauthn.last_seen_on": "Laatst gebruikt",
|
"page.settings.webauthn.last_seen_on": "Last Used",
|
||||||
"page.settings.webauthn.register": "Passkey registreren",
|
"page.settings.webauthn.register": "Wachtwoord registreren",
|
||||||
"page.settings.webauthn.register.error": "Kan passkey niet registreren",
|
"page.settings.webauthn.register.error": "Kan wachtwoord niet registreren",
|
||||||
"page.settings.webauthn.delete": [
|
"page.settings.webauthn.delete": [
|
||||||
"Verwijder %d passkey",
|
"Verwijder %d wachtwoord",
|
||||||
"Verwijder %d passkeys"
|
"Verwijder %d wachtwoordsleutels"
|
||||||
],
|
],
|
||||||
"page.login.oidc_signin": "Inloggen met %s",
|
"page.login.oidc_signin": "Inloggen via OpenID Connect",
|
||||||
"page.login.webauthn_login": "Inloggen met passkey",
|
"page.login.webauthn_login": "Inloggen met wachtwoord",
|
||||||
"page.login.webauthn_login.error": "Kan niet inloggen met passkey",
|
"page.login.webauthn_login.error": "Kan niet inloggen met wachtwoord",
|
||||||
"page.login.google_signin": "Inloggen met Google",
|
"page.login.google_signin": "Inloggen via Google",
|
||||||
"page.integrations.title": "Integraties",
|
"page.integrations.title": "Integraties",
|
||||||
"page.integration.miniflux_api": "Miniflux API",
|
"page.integration.miniflux_api": "Miniflux API",
|
||||||
"page.integration.miniflux_api_endpoint": "API-URL",
|
"page.integration.miniflux_api_endpoint": "API-URL",
|
||||||
@ -238,7 +233,7 @@
|
|||||||
"page.integration.bookmarklet": "Bookmarklet",
|
"page.integration.bookmarklet": "Bookmarklet",
|
||||||
"page.integration.bookmarklet.name": "Toevoegen aan Miniflux",
|
"page.integration.bookmarklet.name": "Toevoegen aan Miniflux",
|
||||||
"page.integration.bookmarklet.instructions": "Sleep deze link naar je bookmarks.",
|
"page.integration.bookmarklet.instructions": "Sleep deze link naar je bookmarks.",
|
||||||
"page.integration.bookmarklet.help": "Gebruik deze link als bookmark in je browser om je direct te abonneren op een website.",
|
"page.integration.bookmarklet.help": "Gebruik deze link als bookmark in je browser om je direct te abboneren op een website.",
|
||||||
"page.sessions.title": "Sessies",
|
"page.sessions.title": "Sessies",
|
||||||
"page.sessions.table.date": "Datum",
|
"page.sessions.table.date": "Datum",
|
||||||
"page.sessions.table.ip": "IP-adres",
|
"page.sessions.table.ip": "IP-adres",
|
||||||
@ -246,8 +241,8 @@
|
|||||||
"page.sessions.table.actions": "Acties",
|
"page.sessions.table.actions": "Acties",
|
||||||
"page.sessions.table.current_session": "Huidige sessie",
|
"page.sessions.table.current_session": "Huidige sessie",
|
||||||
"page.api_keys.title": "API-sleutels",
|
"page.api_keys.title": "API-sleutels",
|
||||||
"page.api_keys.table.description": "Omschrijving",
|
"page.api_keys.table.description": "Beschrijving",
|
||||||
"page.api_keys.table.token": "Token",
|
"page.api_keys.table.token": "Blijk",
|
||||||
"page.api_keys.table.last_used_at": "Laatst gebruikt",
|
"page.api_keys.table.last_used_at": "Laatst gebruikt",
|
||||||
"page.api_keys.table.created_at": "Aanmaakdatum",
|
"page.api_keys.table.created_at": "Aanmaakdatum",
|
||||||
"page.api_keys.table.actions": "Acties",
|
"page.api_keys.table.actions": "Acties",
|
||||||
@ -256,157 +251,133 @@
|
|||||||
"page.offline.title": "Offline modus",
|
"page.offline.title": "Offline modus",
|
||||||
"page.offline.message": "Je bent offline",
|
"page.offline.message": "Je bent offline",
|
||||||
"page.offline.refresh_page": "Probeer de pagina te vernieuwen",
|
"page.offline.refresh_page": "Probeer de pagina te vernieuwen",
|
||||||
"page.webauthn_rename.title": "Hernoem Passkey",
|
"page.webauthn_rename.title": "Rename Passkey",
|
||||||
"alert.no_shared_entry": "Er is geen gedeeld artikel.",
|
"alert.no_shared_entry": "Er is geen gedeelde toegang.",
|
||||||
"alert.no_bookmark": "Er zijn geen favorieten.",
|
"alert.no_bookmark": "Er zijn op dit moment geen favorieten.",
|
||||||
"alert.no_category": "Er zijn geen categorieën.",
|
"alert.no_category": "Er zijn geen categorieën.",
|
||||||
"alert.no_category_entry": "Er zijn geen artikelen in deze categorie.",
|
"alert.no_category_entry": "Deze categorie bevat geen feeds.",
|
||||||
"alert.no_tag_entry": "Er zijn geen artikelen die overeenkomen met deze tag.",
|
|
||||||
"alert.no_feed_entry": "Er zijn geen artikelen in deze feed.",
|
"alert.no_feed_entry": "Er zijn geen artikelen in deze feed.",
|
||||||
"alert.no_feed": "Je hebt nog geen feed geabonneerd.",
|
"alert.no_feed": "Je hebt nog geen feeds geabboneerd staan.",
|
||||||
"alert.no_feed_in_category": "Er is geen feed voor deze categorie.",
|
"alert.no_feed_in_category": "Er is geen abonnement voor deze categorie.",
|
||||||
"alert.no_history": "Geschiedenis is op dit moment leeg.",
|
"alert.no_history": "Geschiedenis is op dit moment leeg.",
|
||||||
"alert.feed_error": "Er is een probleem met deze feed",
|
"alert.feed_error": "Er is een probleem met deze feed",
|
||||||
"alert.no_search_result": "Er is geen resultaat voor deze zoekopdracht.",
|
"alert.no_search_result": "Er is geen resultaat voor deze zoekopdracht.",
|
||||||
"alert.no_unread_entry": "Er zijn geen ongelezen artikelen.",
|
"alert.no_unread_entry": "Er zijn geen ongelezen artikelen.",
|
||||||
"alert.no_user": "Je bent de enige gebruiker.",
|
"alert.no_user": "Je bent de enige gebruiker.",
|
||||||
"alert.account_unlinked": "Jouw externe account is nu ontkoppeld!",
|
"alert.account_unlinked": "Uw externe account is nu gedissocieerd!",
|
||||||
"alert.account_linked": "Jouw externe account is nu gekoppeld!",
|
"alert.account_linked": "Uw externe account is nu gekoppeld!",
|
||||||
"alert.pocket_linked": "Jouw Pocket-account is nu gekoppeld!",
|
"alert.pocket_linked": "Uw Pocket-account is nu gekoppeld!",
|
||||||
"alert.prefs_saved": "Instellingen opgeslagen!",
|
"alert.prefs_saved": "Instellingen opgeslagen!",
|
||||||
"error.unlink_account_without_password": "Je moet een wachtwoord opgeven anders kun je niet meer inloggen.",
|
"error.unlink_account_without_password": "U moet een wachtwoord definiëren anders kunt u zich niet opnieuw aanmelden.",
|
||||||
"error.duplicate_linked_account": "Er is al iemand geregistreerd met deze provider!",
|
"error.duplicate_linked_account": "Er is al iemand geregistreerd met deze provider!",
|
||||||
"error.duplicate_fever_username": "Er is al iemand met dezelfde Fever gebruikersnaam!",
|
"error.duplicate_fever_username": "Er is al iemand met dezelfde Fever gebruikersnaam!",
|
||||||
"error.duplicate_googlereader_username": "Er is al iemand met dezelfde Google Reader gebruikersnaam!",
|
"error.duplicate_googlereader_username": "Er is al iemand met dezelfde Google Reader gebruikersnaam!",
|
||||||
"error.pocket_request_token": "Kon geen aanvraagtoken ophalen van Pocket!",
|
"error.pocket_request_token": "Kon geen aanvraagtoken ophalen van Pocket!",
|
||||||
"error.pocket_access_token": "Kon geen toegangstoken ophalen van Pocket!",
|
"error.pocket_access_token": "Kon geen toegangstoken ophalen van Pocket!",
|
||||||
"error.category_already_exists": "Deze categorie bestaat al.",
|
"error.category_already_exists": "Deze categorie bestaat al.",
|
||||||
"error.unable_to_create_category": "Kan deze categorie niet aanmaken.",
|
"error.unable_to_create_category": "Kan deze categorie niet maken.",
|
||||||
"error.unable_to_update_category": "Kan categorie niet bijwerken.",
|
"error.unable_to_update_category": "Kon categorie niet updaten.",
|
||||||
"error.user_already_exists": "Deze gebruiker bestaat al.",
|
"error.user_already_exists": "Deze gebruiker bestaat al.",
|
||||||
"error.unable_to_create_user": "Kan deze gebruiker niet aanmaken.",
|
"error.unable_to_create_user": "Kan deze gebruiker niet maken.",
|
||||||
"error.unable_to_update_user": "Kan deze gebruiker niet bijwerken.",
|
"error.unable_to_update_user": "Kan deze gebruiker niet updaten.",
|
||||||
"error.unable_to_update_feed": "Kan deze feed niet bijwerken.",
|
"error.unable_to_update_feed": "Kan deze feed niet bijwerken.",
|
||||||
"error.subscription_not_found": "Kan geen feeds vinden.",
|
"error.subscription_not_found": "Kon geen feeds vinden.",
|
||||||
"error.empty_file": "Dit bestand is leeg.",
|
"error.empty_file": "Dit bestand is leeg.",
|
||||||
"error.bad_credentials": "Onjuiste gebruikersnaam of wachtwoord.",
|
"error.bad_credentials": "Onjuiste gebruikersnaam of wachtwoord.",
|
||||||
"error.fields_mandatory": "Alle velden moeten ingevuld zijn.",
|
"error.fields_mandatory": "Alle velden moeten ingevuld zijn.",
|
||||||
"error.title_required": "De titel is verplicht.",
|
"error.title_required": "Naam van categorie is verplicht.",
|
||||||
"error.different_passwords": "Wachtwoorden zijn niet hetzelfde.",
|
"error.different_passwords": "Wachtwoorden zijn niet hetzelfde.",
|
||||||
"error.password_min_length": "Minimaal 6 tekens gebruiken.",
|
"error.password_min_length": "Je moet minstens 6 tekens gebruiken.",
|
||||||
"error.settings_mandatory_fields": "Gebruikersnaam, thema, taal en tijdzone zijn verplichte velden.",
|
"error.settings_mandatory_fields": "Gebruikersnaam, skin, taal en tijdzone zijn verplicht.",
|
||||||
"error.settings_reading_speed_is_positive": "De leessnelheden moeten positieve gehele getallen zijn.",
|
"error.settings_reading_speed_is_positive": "De leessnelheden moeten positieve gehele getallen zijn.",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Ongeldige blokkeerregel: regel #%d mist een geldige veldnaam (Opties: %s)",
|
"error.entries_per_page_invalid": "Het aantal inzendingen per pagina is niet geldig.",
|
||||||
"error.settings_block_rule_separator_required": "Ongeldige blokkeerregel: het patroon van regel #%d moet worden gescheiden door een '='",
|
"error.feed_mandatory_fields": "The URL en de categorie zijn verplicht.",
|
||||||
"error.settings_block_rule_regex_required": "Ongeldige blokkeerregel: het patroon van regel #%d is niet opgegeven",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Ongeldige blokkeerregel: het patroon van regel #%d is geen geldige regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Ongeldige bewaarregel: regel #%d mist een geldige veldnaam (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Ongeldige bewaarregel: het patroon van regel #%d moet worden gescheiden door een '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Ongeldige bewaarregel: het patroon van regel #%d is niet opgegeven",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Ongeldige bewaarregel: het patroon van regel #%d is geen geldige regex",
|
|
||||||
"error.entries_per_page_invalid": "Het aantal artikelen per pagina is niet geldig.",
|
|
||||||
"error.feed_mandatory_fields": "De velden URL en categorie zijn verplicht.",
|
|
||||||
"error.feed_already_exists": "Deze feed bestaat al.",
|
"error.feed_already_exists": "Deze feed bestaat al.",
|
||||||
"error.invalid_feed_url": "Ongeldige feed URL.",
|
"error.invalid_feed_url": "Ongeldige feed-URL.",
|
||||||
"error.invalid_site_url": "Ongeldige site URL.",
|
"error.invalid_site_url": "Ongeldige site-URL.",
|
||||||
"error.feed_url_not_empty": "De feed URL mag niet leeg zijn.",
|
"error.feed_url_not_empty": "De feed-URL mag niet leeg zijn.",
|
||||||
"error.site_url_not_empty": "De site URL mag niet leeg zijn.",
|
"error.site_url_not_empty": "De site-URL mag niet leeg zijn.",
|
||||||
"error.feed_title_not_empty": "De feed titel mag niet leeg zijn.",
|
"error.feed_title_not_empty": "De feedtitel mag niet leeg zijn.",
|
||||||
"error.feed_category_not_found": "Deze categorie bestaat niet of behoort niet tot deze gebruiker.",
|
"error.feed_category_not_found": "Deze categorie bestaat niet of behoort niet tot deze gebruiker.",
|
||||||
"error.feed_invalid_blocklist_rule": "De blokkeerregel is ongeldig.",
|
"error.feed_invalid_blocklist_rule": "De regel voor de blokkeerlijst is ongeldig.",
|
||||||
"error.feed_invalid_keeplist_rule": "De bewaarregel is ongeldig.",
|
"error.feed_invalid_keeplist_rule": "De regel voor het bewaren van een lijst is ongeldig.",
|
||||||
"error.user_mandatory_fields": "Gebruikersnaam is verplicht",
|
"error.user_mandatory_fields": "Gebruikersnaam is verplicht",
|
||||||
"error.api_key_already_exists": "Deze API-sleutel bestaat al.",
|
"error.api_key_already_exists": "This API Key already exists.",
|
||||||
"error.unable_to_create_api_key": "Kan deze API-sleutel niet aanmaken.",
|
"error.unable_to_create_api_key": "Kan deze API-sleutel niet maken.",
|
||||||
"error.invalid_theme": "Ongeldig thema.",
|
"error.invalid_theme": "Ongeldig thema.",
|
||||||
"error.invalid_language": "Ongeldige taal.",
|
"error.invalid_language": "Ongeldige taal.",
|
||||||
"error.invalid_timezone": "Ongeldige tijdzone.",
|
"error.invalid_timezone": "Ongeldige tijdzone.",
|
||||||
"error.invalid_entry_direction": "Ongeldige sorteervolgorde.",
|
"error.invalid_entry_direction": "Ongeldige sorteervolgorde.",
|
||||||
"error.invalid_display_mode": "Ongeldige weergavemodus voor de webapp.",
|
"error.invalid_display_mode": "Ongeldige weergavemodus voor webapp.",
|
||||||
"error.invalid_gesture_nav": "Ongeldige gebarennavigatie.",
|
"error.invalid_gesture_nav": "Ongeldige gebarennavigatie.",
|
||||||
"error.invalid_default_home_page": "Ongeldige startpagina!",
|
"error.invalid_default_home_page": "Ongeldige standaard homepage!",
|
||||||
"form.feed.label.title": "Titel",
|
"form.feed.label.title": "Naam",
|
||||||
"form.feed.label.site_url": "Website URL",
|
"form.feed.label.site_url": "Website URL",
|
||||||
"form.feed.label.feed_url": "Feed URL",
|
"form.feed.label.feed_url": "Feed URL",
|
||||||
"form.feed.label.description": "Omschrijving",
|
|
||||||
"form.feed.label.category": "Categorie",
|
"form.feed.label.category": "Categorie",
|
||||||
"form.feed.label.crawler": "Download originele inhoud",
|
"form.feed.label.crawler": "Download originele content",
|
||||||
"form.feed.label.feed_username": "Feed gebruikersnaam",
|
"form.feed.label.feed_username": "Feed-gebruikersnaam",
|
||||||
"form.feed.label.feed_password": "Feed wachtwoord",
|
"form.feed.label.feed_password": "Feed wachtwoord",
|
||||||
"form.feed.label.user_agent": "Standaard User-agent overschrijven",
|
"form.feed.label.user_agent": "Standaard User Agent overschrijven",
|
||||||
"form.feed.label.cookie": "Cookies instellen",
|
"form.feed.label.cookie": "Cookies instellen",
|
||||||
"form.feed.label.scraper_rules": "Extractieregels",
|
"form.feed.label.scraper_rules": "Scraper regels",
|
||||||
"form.feed.label.rewrite_rules": "Herschrijfregels",
|
"form.feed.label.rewrite_rules": "Rewrite regels",
|
||||||
"form.feed.label.blocklist_rules": "Blokkeerregels",
|
"form.feed.label.blocklist_rules": "Blokkeer regels",
|
||||||
"form.feed.label.keeplist_rules": "Bewaarregels",
|
"form.feed.label.keeplist_rules": "toestemmingsregels",
|
||||||
"form.feed.label.urlrewrite_rules": "Herschrijfregels voor URL's",
|
"form.feed.label.urlrewrite_rules": "Regels voor het herschrijven van URL's",
|
||||||
"form.feed.label.apprise_service_urls": "Door komma's gescheiden lijst van Apprise service URL's",
|
"form.feed.label.apprise_service_urls": "Comma separated list of Apprise service URLs",
|
||||||
"form.feed.label.ignore_http_cache": "Negeer HTTP-cache",
|
"form.feed.label.ignore_http_cache": "Negeer HTTP-cache",
|
||||||
"form.feed.label.allow_self_signed_certificates": "Zelfondertekende of ongeldige certificaten toestaan",
|
"form.feed.label.allow_self_signed_certificates": "Sta zelfondertekende of ongeldige certificaten toe",
|
||||||
"form.feed.label.disable_http2": "HTTP/2 uitschakelen om fingerprinting te voorkomen",
|
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||||
"form.feed.label.fetch_via_proxy": "Ophalen via proxy",
|
"form.feed.label.fetch_via_proxy": "Ophalen via proxy",
|
||||||
"form.feed.label.disabled": "Deze feed niet vernieuwen",
|
"form.feed.label.disabled": "Vernieuw deze feed niet",
|
||||||
"form.feed.label.no_media_player": "Geen mediaspeler (audio/video)",
|
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||||
"form.feed.label.hide_globally": "Verberg artikelen in de globale ongelezen lijst",
|
"form.feed.label.hide_globally": "Verberg items in de globale ongelezen lijst",
|
||||||
"form.feed.label.ntfy_activate": "Artikelen naar ntfy sturen",
|
"form.feed.fieldset.general": "General",
|
||||||
"form.feed.label.ntfy_priority": "Ntfy prioriteit",
|
"form.feed.fieldset.rules": "Rules",
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy maximale prioriteit",
|
"form.feed.fieldset.network_settings": "Network Settings",
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy hoge prioriteit",
|
"form.feed.fieldset.integration": "Third-Party Services",
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy standaard prioriteit",
|
"form.category.label.title": "Naam",
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy lage prioriteit",
|
"form.category.hide_globally": "Verberg items in de globale ongelezen lijst",
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy minimale prioriteit",
|
|
||||||
"form.feed.fieldset.general": "Algemeen",
|
|
||||||
"form.feed.fieldset.rules": "Regels",
|
|
||||||
"form.feed.fieldset.network_settings": "Netwerk Instellingen",
|
|
||||||
"form.feed.fieldset.integration": "Diensten van derden",
|
|
||||||
"form.category.label.title": "Titel",
|
|
||||||
"form.category.hide_globally": "Verberg artikelen in de globale ongelezen lijst",
|
|
||||||
"form.user.label.username": "Gebruikersnaam",
|
"form.user.label.username": "Gebruikersnaam",
|
||||||
"form.user.label.password": "Wachtwoord",
|
"form.user.label.password": "Wachtwoord",
|
||||||
"form.user.label.confirmation": "Bevestig wachtwoord",
|
"form.user.label.confirmation": "Bevestig wachtwoord",
|
||||||
"form.user.label.admin": "Beheerder",
|
"form.user.label.admin": "Administrator",
|
||||||
"form.prefs.label.language": "Taal",
|
"form.prefs.label.language": "Taal",
|
||||||
"form.prefs.label.timezone": "Tijdzone",
|
"form.prefs.label.timezone": "Tijdzone",
|
||||||
"form.prefs.label.theme": "Thema",
|
"form.prefs.label.theme": "Skin",
|
||||||
"form.prefs.label.entry_sorting": "Volgorde van artikelen",
|
"form.prefs.label.entry_sorting": "Volgorde van items",
|
||||||
"form.prefs.label.entries_per_page": "Artikelen per pagina",
|
"form.prefs.label.entries_per_page": "Inzendingen per pagina",
|
||||||
"form.prefs.label.default_reading_speed": "Leessnelheid voor andere talen (woorden per minuut)",
|
"form.prefs.label.default_reading_speed": "Leessnelheid voor andere talen (woorden per minuut)",
|
||||||
"form.prefs.label.cjk_reading_speed": "Leessnelheid voor Chinees, Koreaans en Japans (tekens per minuut)",
|
"form.prefs.label.cjk_reading_speed": "Leessnelheid voor Chinees, Koreaans en Japans (tekens per minuut)",
|
||||||
"form.prefs.label.display_mode": "Weergavemodus Progressive Web App (PWA).",
|
"form.prefs.label.display_mode": "Weergavemodus Progressive Web App (PWA).",
|
||||||
"form.prefs.select.older_first": "Oudere artikelen eerst",
|
"form.prefs.select.older_first": "Oudere items eerst",
|
||||||
"form.prefs.select.recent_first": "Recente artikelen eerst",
|
"form.prefs.select.recent_first": "Recente items eerst",
|
||||||
"form.prefs.select.fullscreen": "Volledig scherm",
|
"form.prefs.select.fullscreen": "Volledig scherm",
|
||||||
"form.prefs.select.standalone": "Standalone",
|
"form.prefs.select.standalone": "Standalone",
|
||||||
"form.prefs.select.minimal_ui": "Minimaal",
|
"form.prefs.select.minimal_ui": "Minimaal",
|
||||||
"form.prefs.select.browser": "Browser",
|
"form.prefs.select.browser": "Browser",
|
||||||
"form.prefs.select.publish_time": "Tijdstip van publiceren artikel",
|
"form.prefs.select.publish_time": "Tijd van binnenkomst",
|
||||||
"form.prefs.select.created_time": "Tijdstip van aanmaken artikel",
|
"form.prefs.select.created_time": "Tijdstip van binnenkomst",
|
||||||
"form.prefs.select.alphabetical": "Alfabetisch",
|
"form.prefs.select.alphabetical": "Alfabetisch",
|
||||||
"form.prefs.select.unread_count": "Aantal ongelezen artikelen",
|
"form.prefs.select.unread_count": "Ongelezen tellen",
|
||||||
"form.prefs.select.none": "Geen",
|
"form.prefs.select.none": "Geen",
|
||||||
"form.prefs.select.tap": "Dubbeltik",
|
"form.prefs.select.tap": "Dubbeltik",
|
||||||
"form.prefs.select.swipe": "Vegen",
|
"form.prefs.select.swipe": "Vegen",
|
||||||
"form.prefs.label.keyboard_shortcuts": "Sneltoetsen inschakelen",
|
"form.prefs.label.keyboard_shortcuts": "Schakel sneltoetsen in",
|
||||||
"form.prefs.label.entry_swipe": "Vegen tussen artikelen inschakelen op aanraakschermen",
|
"form.prefs.label.entry_swipe": "Invoervegen inschakelen op aanraakschermen",
|
||||||
"form.prefs.label.gesture_nav": "Gebaar om tussen artikelen te navigeren",
|
"form.prefs.label.gesture_nav": "Gebaar om tussen ingangen te navigeren",
|
||||||
"form.prefs.label.show_reading_time": "Toon geschatte leestijd van artikelen",
|
"form.prefs.label.show_reading_time": "Toon geschatte leestijd voor artikelen",
|
||||||
"form.prefs.label.custom_css": "Aangepaste CSS",
|
"form.prefs.label.custom_css": "Aangepaste CSS",
|
||||||
"form.prefs.label.entry_order": "Artikelen sorteren",
|
"form.prefs.label.entry_order": "Ingang Sorteerkolom",
|
||||||
"form.prefs.label.default_home_page": "Startpagina",
|
"form.prefs.label.default_home_page": "Standaard startpagina",
|
||||||
"form.prefs.label.categories_sorting_order": "Volgorde categorieën",
|
"form.prefs.label.categories_sorting_order": "Categorieën sorteren",
|
||||||
"form.prefs.label.mark_read_on_view": "Markeer artikelen automatisch als gelezen wanneer ze worden bekeken",
|
"form.prefs.label.mark_read_on_view": "Items automatisch markeren als gelezen wanneer ze worden bekeken",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Markeer artikelen als gelezen wanneer ze worden bekeken. Voor audio/video, markeer als gelezen bij 90%% voltooiing",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Markeer artikelen alleen als gelezen wanneer het afspelen van audio/video 90%% heeft bereikt",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.label.mark_read_manually": "Markeer artikelen handmatig als gelezen",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
"form.prefs.fieldset.application_settings": "Applicatie Instellingen",
|
|
||||||
"form.prefs.fieldset.authentication_settings": "Authenticatie Instellingen",
|
|
||||||
"form.prefs.fieldset.reader_settings": "Lees Instellingen",
|
|
||||||
"form.prefs.fieldset.global_feed_settings": "Globale Feed Instellingen",
|
|
||||||
"form.import.label.file": "OPML-bestand",
|
"form.import.label.file": "OPML-bestand",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "Artikelen opslaan in Betula",
|
|
||||||
"form.integration.betula_url": "Betula server URL",
|
|
||||||
"form.integration.betula_token": "Betula Token",
|
|
||||||
"form.integration.fever_activate": "Activeer Fever API",
|
"form.integration.fever_activate": "Activeer Fever API",
|
||||||
"form.integration.fever_username": "Fever gebruikersnaam",
|
"form.integration.fever_username": "Fever gebruikersnaam",
|
||||||
"form.integration.fever_password": "Fever wachtwoord",
|
"form.integration.fever_password": "Fever wachtwoord",
|
||||||
@ -414,102 +385,91 @@
|
|||||||
"form.integration.googlereader_activate": "Activeer Google Reader API",
|
"form.integration.googlereader_activate": "Activeer Google Reader API",
|
||||||
"form.integration.googlereader_username": "Google Reader gebruikersnaam",
|
"form.integration.googlereader_username": "Google Reader gebruikersnaam",
|
||||||
"form.integration.googlereader_password": "Google Reader wachtwoord",
|
"form.integration.googlereader_password": "Google Reader wachtwoord",
|
||||||
"form.integration.googlereader_endpoint": "Google Reader API-endpoint:",
|
"form.integration.googlereader_endpoint": "Google Reader URL:",
|
||||||
"form.integration.pinboard_activate": "Artikelen opslaan in Pinboard",
|
"form.integration.pinboard_activate": "Artikelen opslaan naar Pinboard",
|
||||||
"form.integration.pinboard_token": "Pinboard API token",
|
"form.integration.pinboard_token": "Pinboard API token",
|
||||||
"form.integration.pinboard_tags": "Pinboard tags",
|
"form.integration.pinboard_tags": "Pinboard tags",
|
||||||
"form.integration.pinboard_bookmark": "Markeer favoriet als ongelezen",
|
"form.integration.pinboard_bookmark": "Markeer bookmark als gelezen",
|
||||||
"form.integration.instapaper_activate": "Artikelen opslaan in Instapaper",
|
"form.integration.instapaper_activate": "Artikelen opstaan naar Instapaper",
|
||||||
"form.integration.instapaper_username": "Instapaper gebruikersnaam",
|
"form.integration.instapaper_username": "Instapaper gebruikersnaam",
|
||||||
"form.integration.instapaper_password": "Instapaper wachtwoord",
|
"form.integration.instapaper_password": "Instapaper wachtwoord",
|
||||||
"form.integration.pocket_activate": "Artikelen opslaan in Pocket",
|
"form.integration.pocket_activate": "Bewaar artikelen in Pocket",
|
||||||
"form.integration.pocket_consumer_key": "Pocket Consumer Key",
|
"form.integration.pocket_consumer_key": "Pocket Consumer Key",
|
||||||
"form.integration.pocket_access_token": "Pocket Access Token",
|
"form.integration.pocket_access_token": "Pocket Access Token",
|
||||||
"form.integration.pocket_connect_link": "Verbind je Pocket-account",
|
"form.integration.pocket_connect_link": "Verbind je Pocket-account",
|
||||||
"form.integration.wallabag_activate": "Artikelen opslaan in Wallabag",
|
"form.integration.wallabag_activate": "Opslaan naar Wallabag",
|
||||||
"form.integration.wallabag_only_url": "Alleen URL verzenden (in plaats van volledige inhoud)",
|
"form.integration.wallabag_only_url": "Alleen URL verzenden (in plaats van volledige inhoud)",
|
||||||
"form.integration.wallabag_endpoint": "Wallabag URL",
|
"form.integration.wallabag_endpoint": "Wallabag URL",
|
||||||
"form.integration.wallabag_client_id": "Wallabag Client-ID",
|
"form.integration.wallabag_client_id": "Wallabag Client-ID",
|
||||||
"form.integration.wallabag_client_secret": "Wallabag Client-Secret",
|
"form.integration.wallabag_client_secret": "Wallabag Client-Secret",
|
||||||
"form.integration.wallabag_username": "Wallabag gebruikersnaam",
|
"form.integration.wallabag_username": "Wallabag gebruikersnaam",
|
||||||
"form.integration.wallabag_password": "Wallabag wachtwoord",
|
"form.integration.wallabag_password": "Wallabag wachtwoord",
|
||||||
"form.integration.notion_activate": "Artikelen opslaan in Notion",
|
"form.integration.notion_activate": "Save entries to Notion",
|
||||||
"form.integration.notion_page_id": "Notion Page ID",
|
"form.integration.notion_page_id": "Notion Page ID",
|
||||||
"form.integration.notion_token": "Notion Secret Token",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "Artikelen opslaan in Apprise",
|
"form.integration.apprise_activate": "Push entries to Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"form.integration.apprise_url": "Apprise API URL",
|
||||||
"form.integration.apprise_services_url": "Door komma's gescheiden lijst van Apprise service URL's",
|
"form.integration.apprise_services_url": "Comma separated list of Apprise service URLs",
|
||||||
"form.integration.nunux_keeper_activate": "Artikelen opslaan in Nunux Keeper",
|
"form.integration.nunux_keeper_activate": "Opslaan naar Nunux Keeper",
|
||||||
"form.integration.nunux_keeper_endpoint": "Nunux Keeper URL",
|
"form.integration.nunux_keeper_endpoint": "Nunux Keeper URL",
|
||||||
"form.integration.nunux_keeper_api_key": "Nunux Keeper API-sleutel",
|
"form.integration.nunux_keeper_api_key": "Nunux Keeper API-sleutel",
|
||||||
"form.integration.omnivore_activate": "Artikelen opslaan in Omnivore",
|
"form.integration.omnivore_activate": "Opslaan naar Omnivore",
|
||||||
"form.integration.omnivore_url": "Omnivore URL",
|
"form.integration.omnivore_url": "Omnivore URL",
|
||||||
"form.integration.omnivore_api_key": "Omnivore API-sleutel",
|
"form.integration.omnivore_api_key": "Omnivore API-sleutel",
|
||||||
"form.integration.espial_activate": "Artikelen opslaan in Espial",
|
"form.integration.espial_activate": "Opslaan naar Espial",
|
||||||
"form.integration.espial_endpoint": "Espial URL",
|
"form.integration.espial_endpoint": "Espial URL",
|
||||||
"form.integration.espial_api_key": "Espial API-sleutel",
|
"form.integration.espial_api_key": "Espial API-sleutel",
|
||||||
"form.integration.espial_tags": "Espial tags",
|
"form.integration.espial_tags": "Espial tags",
|
||||||
"form.integration.readwise_activate": "Artikelen opslaan in Readwise Reader",
|
"form.integration.readwise_activate": "Save entries to Readwise Reader",
|
||||||
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||||
"form.integration.readwise_api_key_link": "Readwise Access Token ophalen",
|
"form.integration.readwise_api_key_link": "Get your Readwise Access Token",
|
||||||
"form.integration.telegram_bot_activate": "Stuur nieuwe artikelen naar Telegram",
|
"form.integration.telegram_bot_activate": "Push nieuwe artikelen naar Telegram-chat",
|
||||||
"form.integration.telegram_bot_token": "Bot token",
|
"form.integration.telegram_bot_token": "Bot token",
|
||||||
"form.integration.telegram_chat_id": "Chat ID",
|
"form.integration.telegram_chat_id": "Chat ID",
|
||||||
"form.integration.telegram_topic_id": "Topic ID",
|
"form.integration.telegram_topic_id": "Topic ID",
|
||||||
"form.integration.telegram_bot_disable_web_page_preview": "Webpaginavoorbeeld uitschakelen",
|
"form.integration.telegram_bot_disable_web_page_preview": "Disable web page preview",
|
||||||
"form.integration.telegram_bot_disable_notification": "Notificatie uitschakelen",
|
"form.integration.telegram_bot_disable_notification": "Disable notification",
|
||||||
"form.integration.telegram_bot_disable_buttons": "Knoppen uitschakelen",
|
"form.integration.telegram_bot_disable_buttons": "Disable buttons",
|
||||||
"form.integration.linkace_activate": "Artikelen opslaan in LinkAce",
|
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||||
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||||
"form.integration.linkace_api_key": "LinkAce API-sleutel",
|
"form.integration.linkace_api_key": "LinkAce API key",
|
||||||
"form.integration.linkace_tags": "LinkAce tags",
|
"form.integration.linkace_tags": "LinkAce Tags",
|
||||||
"form.integration.linkace_is_private": "Koppeling als privé markeren",
|
"form.integration.linkace_is_private": "Mark link as private",
|
||||||
"form.integration.linkace_check_disabled": "Koppelingcontrole uitschakelen",
|
"form.integration.linkace_check_disabled": "Disable link check",
|
||||||
"form.integration.linkding_activate": "Artikelen opslaan in Linkding",
|
"form.integration.linkding_activate": "Opslaan naar Linkding",
|
||||||
"form.integration.linkding_endpoint": "Linkding URL",
|
"form.integration.linkding_endpoint": "Linkding URL",
|
||||||
"form.integration.linkding_api_key": "Linkding API-sleutel",
|
"form.integration.linkding_api_key": "Linkding API-sleutel",
|
||||||
"form.integration.linkding_tags": "Linkding tags",
|
"form.integration.linkding_tags": "Linkding Tags",
|
||||||
"form.integration.linkding_bookmark": "Markeer favoriet als ongelezen",
|
"form.integration.linkding_bookmark": "Markeer bookmark als gelezen",
|
||||||
"form.integration.linkwarden_activate": "Artikelen opslaan in Linkwarden",
|
"form.integration.linkwarden_activate": "Opslaan naar Linkwarden",
|
||||||
"form.integration.linkwarden_endpoint": "Linkwarden URL",
|
"form.integration.linkwarden_endpoint": "Linkwarden URL",
|
||||||
"form.integration.linkwarden_api_key": "Linkwarden API-sleutel",
|
"form.integration.linkwarden_api_key": "Linkwarden API-sleutel",
|
||||||
"form.integration.matrix_bot_activate": "Nieuwe artikelen opslaan in Matrix",
|
"form.integration.matrix_bot_activate": "Nieuwe artikelen overbrengen naar Matrix",
|
||||||
"form.integration.matrix_bot_user": "Matrix gebruikersnaam",
|
"form.integration.matrix_bot_user": "Gebruikersnaam voor Matrix",
|
||||||
"form.integration.matrix_bot_password": "Wachtwoord voor Matrix-gebruiker",
|
"form.integration.matrix_bot_password": "Wachtwoord voor Matrix-gebruiker",
|
||||||
"form.integration.matrix_bot_url": "URL van de Matrix-server",
|
"form.integration.matrix_bot_url": "URL van de Matrix-server",
|
||||||
"form.integration.matrix_bot_chat_id": "ID van Matrix-kamer",
|
"form.integration.matrix_bot_chat_id": "ID van Matrix-kamer",
|
||||||
"form.integration.raindrop_activate": "Artikelen opslaan in Raindrop",
|
"form.integration.readeck_activate": "Opslaan naar Readeck",
|
||||||
"form.integration.raindrop_token": "Raindrop Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collectie ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (commagescheiden)",
|
|
||||||
"form.integration.readeck_activate": "Artikelen opslaan in Readeck",
|
|
||||||
"form.integration.readeck_endpoint": "Readeck URL",
|
"form.integration.readeck_endpoint": "Readeck URL",
|
||||||
"form.integration.readeck_api_key": "Readeck API-sleutel",
|
"form.integration.readeck_api_key": "Readeck API-sleutel",
|
||||||
"form.integration.readeck_labels": "Readeck Labels",
|
"form.integration.readeck_labels": "Readeck Labels",
|
||||||
"form.integration.readeck_only_url": "Alleen URL verzenden (in plaats van volledige inhoud)",
|
"form.integration.readeck_only_url": "Alleen URL verzenden (in plaats van volledige inhoud)",
|
||||||
"form.integration.shiori_activate": "Artikelen opslaan in Shiori",
|
"form.integration.shiori_activate": "Opslaan naar Shiori",
|
||||||
"form.integration.shiori_endpoint": "Shiori URL",
|
"form.integration.shiori_endpoint": "Shiori URL",
|
||||||
"form.integration.shiori_username": "Shiori gebruikersnaam",
|
"form.integration.shiori_username": "Shiori gebruikersnaam",
|
||||||
"form.integration.shiori_password": "Shiori wachtwoord",
|
"form.integration.shiori_password": "Shiori wachtwoord",
|
||||||
"form.integration.shaarli_activate": "Artikelen opslaan in Shaarli",
|
"form.integration.shaarli_activate": "Save articles to Shaarli",
|
||||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||||
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||||
"form.integration.webhook_activate": "Webhook activeren",
|
"form.integration.webhook_activate": "Enable Webhook",
|
||||||
"form.integration.webhook_url": "Webhook URL",
|
"form.integration.webhook_url": "Webhook URL",
|
||||||
"form.integration.webhook_secret": "Webhook Secret",
|
"form.integration.webhook_secret": "Webhook Secret",
|
||||||
"form.integration.rssbridge_activate": "Controleer RSS-Bridge bij het toevoegen van abonnementen",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Stuur artikelen naar ntfy",
|
"form.api_key.label.description": "API-sleutellabel",
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optioneel, standaard is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optioneel)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy gebruikersnaam (optioneel)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy wachtwoord (optioneel)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optioneel)",
|
|
||||||
"form.api_key.label.description": "API-sleutel omschrijving",
|
|
||||||
"form.submit.loading": "Laden...",
|
"form.submit.loading": "Laden...",
|
||||||
"form.submit.saving": "Opslaan...",
|
"form.submit.saving": "Opslaag...",
|
||||||
"time_elapsed.not_yet": "nog niet",
|
"time_elapsed.not_yet": "in de toekomst",
|
||||||
"time_elapsed.yesterday": "gisteren",
|
"time_elapsed.yesterday": "gisteren",
|
||||||
"time_elapsed.now": "minder dan een minuut geleden",
|
"time_elapsed.now": "minder dan een minuut geleden",
|
||||||
"time_elapsed.minutes": [
|
"time_elapsed.minutes": [
|
||||||
@ -537,43 +497,32 @@
|
|||||||
"%d jaar geleden"
|
"%d jaar geleden"
|
||||||
],
|
],
|
||||||
"alert.too_many_feeds_refresh": [
|
"alert.too_many_feeds_refresh": [
|
||||||
"Je hebt te veel feed-vernieuwingen getriggered. Wacht aub %d minuut voor opnieuw proberen.",
|
"You have triggered too many feed refreshes. Please wait %d minute before trying again.",
|
||||||
"Je hebt te veel feed-vernieuwingen getriggered. Wacht aub %d minuten voor opnieuw proberen."
|
"You have triggered too many feed refreshes. Please wait %d minutes before trying again."
|
||||||
],
|
],
|
||||||
"alert.background_feed_refresh": "Alle feeds worden op de achtergrond vernieuwd. Je kunt Miniflux blijven gebruiker terwijl dit proces draait.",
|
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||||
"error.http_response_too_large": "De HTTP-respons is te groot. Je kunt de limiet voor de HTTP-responsgrootte verhogen in de globale instellingen (server herstart noodzakelijk)",
|
"error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",
|
||||||
"error.http_body_read": "Kan de HTTP-body niet lezen: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "De HTTP-respons body is leeg.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "De HTTP-respons is leeg. Misschien gebruikt deze website een botbeveiligingsmechanisme?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS fout: %q. Als je wilt, kun je TLS-verificatie uitschakelen in de feed-instellingen.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux kan deze website niet bereiken vanwege een netwerkfout: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "Deze website is te traag en de aanvraag gaf timeout: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP-client-fout: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
"error.http_not_authorized": "Toegang tot deze website is niet geautoriseerd. Het kan een foute gebruikersnaam of wachtwoord zijn.",
|
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||||
"error.http_too_many_requests": "Miniflux heeft te veel aanvragen gegenereerd voor deze website. Probeer het later nog eens of wijzig de applicatieconfiguratie.",
|
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||||
"error.http_forbidden": "Toegang tot deze website is verboden. Misschien heeft deze website een botbeveiligingsmechanisme?",
|
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||||
"error.http_resource_not_found": "De gevraagde bron is niet gevonden. Controleer de URL.",
|
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||||
"error.http_internal_server_error": "De website is momenteel niet beschikbaar vanwege een interne-server-fout. De oorzaak hiervan ligt niet bij Miniflux. Probeer het later nogmaals aub.",
|
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.http_bad_gateway": "De website is momenteel niet beschikbaar vanwege een slechte-gateway-fout. De oorzaak hiervan ligt niet bij Miniflux. Probeer het later nogmaals aub.",
|
"error.http_bad_gateway": "The website is not available at the moment due to a bad gateway error. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.http_service_unavailable": "De website is momenteel niet beschikbaar vanwege een interne-server-fout. De oorzaak hiervan ligt niet bij Miniflux. Probeer het later nogmaals aub.",
|
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.http_gateway_timeout": "De website is momenteel niet beschikbaar vanwege een timeout bij de gateway. De oorzaak hiervan ligt niet bij Miniflux. Probeer het later nogmaals aub.",
|
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.http_unexpected_status_code": "De website is momenteel niet beschikbaar vanwege een onverwachte HTTP-statuscode: %d. De oorzaak hiervan ligt niet bij Miniflux. Probeer het later nogmaals aub.",
|
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.database_error": "Database fout: %v.",
|
"error.database_error": "Database error: %v.",
|
||||||
"error.category_not_found": "Deze categorie bestaat niet of hoort niet bij deze gebruiker.",
|
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||||
"error.duplicated_feed": "Deze feed bestaat al.",
|
"error.duplicated_feed": "This feed already exists.",
|
||||||
"error.unable_to_parse_feed": "Kan deze feed niet verwerken: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "Deze feed bestaat niet of is niet van deze gebruiker.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "Kan feed niet detecteren met RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Feed-formaat kan niet worden gedetecteerd: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "Afspeelsnelheid van de audio/video",
|
|
||||||
"error.settings_media_playback_rate_range": "Afspeelsnelheid is buiten bereik",
|
|
||||||
"enclosure_media_controls.seek" : "Vooruit/terug:",
|
|
||||||
"enclosure_media_controls.seek.title" : " Vooruit/terug met %s seconden",
|
|
||||||
"enclosure_media_controls.speed" : "Snelheid:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Versnel",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Versnel met %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Vertraag",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Vertraag met %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset snelheid naar 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "Oznacz wszystko jako przeczytane",
|
"menu.mark_all_as_read": "Oznacz wszystko jako przeczytane",
|
||||||
"menu.show_all_entries": "Pokaż wszystkie artykuły",
|
"menu.show_all_entries": "Pokaż wszystkie artykuły",
|
||||||
"menu.show_only_unread_entries": "Pokaż tylko nieprzeczytane artykuły",
|
"menu.show_only_unread_entries": "Pokaż tylko nieprzeczytane artykuły",
|
||||||
"menu.show_only_starred_entries": "Pokaż tylko ulubione artykuły",
|
|
||||||
"menu.refresh_feed": "Odśwież",
|
"menu.refresh_feed": "Odśwież",
|
||||||
"menu.refresh_all_feeds": "Odśwież wszystkie subskrypcje w tle",
|
"menu.refresh_all_feeds": "Odśwież wszystkie subskrypcje w tle",
|
||||||
"menu.edit_feed": "Edytuj",
|
"menu.edit_feed": "Edytuj",
|
||||||
@ -56,9 +55,7 @@
|
|||||||
"search.label": "Szukaj",
|
"search.label": "Szukaj",
|
||||||
"search.placeholder": "Szukaj...",
|
"search.placeholder": "Szukaj...",
|
||||||
"search.submit": "Search",
|
"search.submit": "Search",
|
||||||
"pagination.last": "Ostatni",
|
|
||||||
"pagination.next": "Następny",
|
"pagination.next": "Następny",
|
||||||
"pagination.first": "Pierwszy",
|
|
||||||
"pagination.previous": "Poprzedni",
|
"pagination.previous": "Poprzedni",
|
||||||
"entry.status.unread": "Nieprzeczytane",
|
"entry.status.unread": "Nieprzeczytane",
|
||||||
"entry.status.read": "Przeczytane",
|
"entry.status.read": "Przeczytane",
|
||||||
@ -190,8 +187,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "Przejdź do subskrypcji",
|
"page.keyboard_shortcuts.go_to_feed": "Przejdź do subskrypcji",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "Przejdź do poprzedniej strony",
|
"page.keyboard_shortcuts.go_to_previous_page": "Przejdź do poprzedniej strony",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "Przejdź do następnej strony",
|
"page.keyboard_shortcuts.go_to_next_page": "Przejdź do następnej strony",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "Przejdź do dolnego elementu",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "Przejdź do najwyższego elementu",
|
|
||||||
"page.keyboard_shortcuts.open_item": "Otwórz zaznaczony artykuł",
|
"page.keyboard_shortcuts.open_item": "Otwórz zaznaczony artykuł",
|
||||||
"page.keyboard_shortcuts.open_original": "Otwórz oryginalny artykuł",
|
"page.keyboard_shortcuts.open_original": "Otwórz oryginalny artykuł",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "Otwórz oryginalny link w bieżącej karcie",
|
"page.keyboard_shortcuts.open_original_same_window": "Otwórz oryginalny link w bieżącej karcie",
|
||||||
@ -220,8 +215,8 @@
|
|||||||
"page.settings.title": "Ustawienia",
|
"page.settings.title": "Ustawienia",
|
||||||
"page.settings.link_google_account": "Połącz z moim kontem Google",
|
"page.settings.link_google_account": "Połącz z moim kontem Google",
|
||||||
"page.settings.unlink_google_account": "Odłącz moje konto Google",
|
"page.settings.unlink_google_account": "Odłącz moje konto Google",
|
||||||
"page.settings.link_oidc_account": "Połącz z moim kontem %s",
|
"page.settings.link_oidc_account": "Połącz z moim kontem OpenID Connect",
|
||||||
"page.settings.unlink_oidc_account": "Odłącz moje konto %s",
|
"page.settings.unlink_oidc_account": "Odłącz moje konto OpenID Connect",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Actions",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||||
@ -236,7 +231,7 @@
|
|||||||
],
|
],
|
||||||
"page.login.title": "Zaloguj się",
|
"page.login.title": "Zaloguj się",
|
||||||
"page.login.google_signin": "Zaloguj przez Google",
|
"page.login.google_signin": "Zaloguj przez Google",
|
||||||
"page.login.oidc_signin": "Zaloguj przez %s",
|
"page.login.oidc_signin": "Zaloguj przez OpenID Connect",
|
||||||
"page.login.webauthn_login": "Zaloguj się za pomocą hasła",
|
"page.login.webauthn_login": "Zaloguj się za pomocą hasła",
|
||||||
"page.login.webauthn_login.error": "Nie można zalogować się za pomocą klucza dostępu",
|
"page.login.webauthn_login.error": "Nie można zalogować się za pomocą klucza dostępu",
|
||||||
"page.integrations.title": "Usługi",
|
"page.integrations.title": "Usługi",
|
||||||
@ -271,7 +266,6 @@
|
|||||||
"alert.no_bookmark": "Obecnie nie ma żadnych zakładek.",
|
"alert.no_bookmark": "Obecnie nie ma żadnych zakładek.",
|
||||||
"alert.no_category": "Nie ma żadnej kategorii!",
|
"alert.no_category": "Nie ma żadnej kategorii!",
|
||||||
"alert.no_category_entry": "W tej kategorii nie ma żadnych artykułów",
|
"alert.no_category_entry": "W tej kategorii nie ma żadnych artykułów",
|
||||||
"alert.no_tag_entry": "Nie ma wpisów pasujących do tego tagu.",
|
|
||||||
"alert.no_feed_entry": "Nie ma artykułu dla tego kanału.",
|
"alert.no_feed_entry": "Nie ma artykułu dla tego kanału.",
|
||||||
"alert.no_feed": "Nie masz żadnej subskrypcji.",
|
"alert.no_feed": "Nie masz żadnej subskrypcji.",
|
||||||
"alert.no_feed_in_category": "Nie ma subskrypcji dla tej kategorii.",
|
"alert.no_feed_in_category": "Nie ma subskrypcji dla tej kategorii.",
|
||||||
@ -306,14 +300,6 @@
|
|||||||
"error.password_min_length": "Musisz użyć co najmniej 6 znaków.",
|
"error.password_min_length": "Musisz użyć co najmniej 6 znaków.",
|
||||||
"error.settings_mandatory_fields": "Pola nazwy użytkownika, tematu, języka i strefy czasowej są obowiązkowe.",
|
"error.settings_mandatory_fields": "Pola nazwy użytkownika, tematu, języka i strefy czasowej są obowiązkowe.",
|
||||||
"error.settings_reading_speed_is_positive": "Prędkości odczytu muszą być dodatnimi liczbami całkowitymi.",
|
"error.settings_reading_speed_is_positive": "Prędkości odczytu muszą być dodatnimi liczbami całkowitymi.",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.entries_per_page_invalid": "Liczba wpisów na stronę jest nieprawidłowa.",
|
"error.entries_per_page_invalid": "Liczba wpisów na stronę jest nieprawidłowa.",
|
||||||
"error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.",
|
"error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.",
|
||||||
"error.feed_already_exists": "Ten kanał już istnieje.",
|
"error.feed_already_exists": "Ten kanał już istnieje.",
|
||||||
@ -338,7 +324,6 @@
|
|||||||
"form.feed.label.title": "Tytuł",
|
"form.feed.label.title": "Tytuł",
|
||||||
"form.feed.label.site_url": "URL strony",
|
"form.feed.label.site_url": "URL strony",
|
||||||
"form.feed.label.feed_url": "URL kanału",
|
"form.feed.label.feed_url": "URL kanału",
|
||||||
"form.feed.label.description": "Opis",
|
|
||||||
"form.feed.label.category": "Kategoria",
|
"form.feed.label.category": "Kategoria",
|
||||||
"form.feed.label.crawler": "Pobierz oryginalną treść",
|
"form.feed.label.crawler": "Pobierz oryginalną treść",
|
||||||
"form.feed.label.feed_username": "Subskrypcję nazwa użytkownika",
|
"form.feed.label.feed_username": "Subskrypcję nazwa użytkownika",
|
||||||
@ -358,13 +343,6 @@
|
|||||||
"form.feed.label.disabled": "Nie odświeżaj tego kanału",
|
"form.feed.label.disabled": "Nie odświeżaj tego kanału",
|
||||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||||
"form.feed.label.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
|
"form.feed.label.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
|
||||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
|
||||||
"form.feed.fieldset.general": "General",
|
"form.feed.fieldset.general": "General",
|
||||||
"form.feed.fieldset.rules": "Rules",
|
"form.feed.fieldset.rules": "Rules",
|
||||||
"form.feed.fieldset.network_settings": "Network Settings",
|
"form.feed.fieldset.network_settings": "Network Settings",
|
||||||
@ -405,18 +383,11 @@
|
|||||||
"form.prefs.label.default_home_page": "Domyślna strona główna",
|
"form.prefs.label.default_home_page": "Domyślna strona główna",
|
||||||
"form.prefs.label.categories_sorting_order": "Sortowanie kategorii",
|
"form.prefs.label.categories_sorting_order": "Sortowanie kategorii",
|
||||||
"form.prefs.label.mark_read_on_view": "Automatycznie oznaczaj wpisy jako przeczytane podczas przeglądania",
|
"form.prefs.label.mark_read_on_view": "Automatycznie oznaczaj wpisy jako przeczytane podczas przeglądania",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
|
||||||
"form.import.label.file": "Plik OPML",
|
"form.import.label.file": "Plik OPML",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "Save entries to Betula",
|
|
||||||
"form.integration.betula_url": "Betula server URL",
|
|
||||||
"form.integration.betula_token": "Betula Token",
|
|
||||||
"form.integration.fever_activate": "Aktywuj Fever API",
|
"form.integration.fever_activate": "Aktywuj Fever API",
|
||||||
"form.integration.fever_username": "Login do Fever",
|
"form.integration.fever_username": "Login do Fever",
|
||||||
"form.integration.fever_password": "Hasło do Fever",
|
"form.integration.fever_password": "Hasło do Fever",
|
||||||
@ -488,10 +459,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Hasło dla użytkownika Matrix",
|
"form.integration.matrix_bot_password": "Hasło dla użytkownika Matrix",
|
||||||
"form.integration.matrix_bot_url": "URL serwera Matrix",
|
"form.integration.matrix_bot_url": "URL serwera Matrix",
|
||||||
"form.integration.matrix_bot_chat_id": "Identyfikator pokoju Matrix",
|
"form.integration.matrix_bot_chat_id": "Identyfikator pokoju Matrix",
|
||||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "Zapisz artykuły do Readeck",
|
"form.integration.readeck_activate": "Zapisz artykuły do Readeck",
|
||||||
"form.integration.readeck_endpoint": "Readeck URL",
|
"form.integration.readeck_endpoint": "Readeck URL",
|
||||||
"form.integration.readeck_api_key": "Readeck API key",
|
"form.integration.readeck_api_key": "Readeck API key",
|
||||||
@ -509,13 +476,6 @@
|
|||||||
"form.integration.webhook_secret": "Webhook Secret",
|
"form.integration.webhook_secret": "Webhook Secret",
|
||||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
|
||||||
"form.api_key.label.description": "Etykieta klucza API",
|
"form.api_key.label.description": "Etykieta klucza API",
|
||||||
"form.submit.loading": "Ładowanie...",
|
"form.submit.loading": "Ładowanie...",
|
||||||
"form.submit.saving": "Zapisywanie...",
|
"form.submit.saving": "Zapisywanie...",
|
||||||
@ -562,7 +522,7 @@
|
|||||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP client error: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
@ -581,16 +541,5 @@
|
|||||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "Prędkość odtwarzania audio/wideo",
|
|
||||||
"error.settings_media_playback_rate_range": "Prędkość odtwarzania jest poza zakresem",
|
|
||||||
"enclosure_media_controls.seek" : "Seek:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Speed:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Faster",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Slower",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "Marcar todos como lido",
|
"menu.mark_all_as_read": "Marcar todos como lido",
|
||||||
"menu.show_all_entries": "Mostrar todas os itens",
|
"menu.show_all_entries": "Mostrar todas os itens",
|
||||||
"menu.show_only_unread_entries": "Mostrar apenas itens não lidos",
|
"menu.show_only_unread_entries": "Mostrar apenas itens não lidos",
|
||||||
"menu.show_only_starred_entries": "Mostrar apenas os favoritos",
|
|
||||||
"menu.refresh_feed": "Atualizar",
|
"menu.refresh_feed": "Atualizar",
|
||||||
"menu.refresh_all_feeds": "Atualizar todas as fontes",
|
"menu.refresh_all_feeds": "Atualizar todas as fontes",
|
||||||
"menu.edit_feed": "Editar",
|
"menu.edit_feed": "Editar",
|
||||||
@ -56,9 +55,7 @@
|
|||||||
"search.label": "Buscar",
|
"search.label": "Buscar",
|
||||||
"search.placeholder": "Buscar por...",
|
"search.placeholder": "Buscar por...",
|
||||||
"search.submit": "Search",
|
"search.submit": "Search",
|
||||||
"pagination.last": "Last",
|
|
||||||
"pagination.next": "Próximo",
|
"pagination.next": "Próximo",
|
||||||
"pagination.first": "First",
|
|
||||||
"pagination.previous": "Anterior",
|
"pagination.previous": "Anterior",
|
||||||
"entry.status.unread": "Não lido",
|
"entry.status.unread": "Não lido",
|
||||||
"entry.status.read": "Lido",
|
"entry.status.read": "Lido",
|
||||||
@ -181,8 +178,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "Ir a fonte",
|
"page.keyboard_shortcuts.go_to_feed": "Ir a fonte",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "Ir a página anterior",
|
"page.keyboard_shortcuts.go_to_previous_page": "Ir a página anterior",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "Ir a página seguinte",
|
"page.keyboard_shortcuts.go_to_next_page": "Ir a página seguinte",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "Ir para o item inferior",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "Ir para o item superior",
|
|
||||||
"page.keyboard_shortcuts.open_item": "Abrir o item selecionado",
|
"page.keyboard_shortcuts.open_item": "Abrir o item selecionado",
|
||||||
"page.keyboard_shortcuts.open_original": "Abrir o conteúdo original",
|
"page.keyboard_shortcuts.open_original": "Abrir o conteúdo original",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "Abrir o conteúdo original na janela atual",
|
"page.keyboard_shortcuts.open_original_same_window": "Abrir o conteúdo original na janela atual",
|
||||||
@ -211,8 +206,8 @@
|
|||||||
"page.settings.title": "Ajustes",
|
"page.settings.title": "Ajustes",
|
||||||
"page.settings.link_google_account": "Vincular minha conta do Google",
|
"page.settings.link_google_account": "Vincular minha conta do Google",
|
||||||
"page.settings.unlink_google_account": "Desvincular minha conta do Google",
|
"page.settings.unlink_google_account": "Desvincular minha conta do Google",
|
||||||
"page.settings.link_oidc_account": "Vincular minha conta do %s",
|
"page.settings.link_oidc_account": "Vincular minha conta do OpenID Connect",
|
||||||
"page.settings.unlink_oidc_account": "Desvincular minha conta do %s",
|
"page.settings.unlink_oidc_account": "Desvincular minha conta do OpenID Connect",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Actions",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||||
@ -226,7 +221,7 @@
|
|||||||
],
|
],
|
||||||
"page.login.title": "Iniciar Sessão",
|
"page.login.title": "Iniciar Sessão",
|
||||||
"page.login.google_signin": "Iniciar Sessão com sua conta do Google",
|
"page.login.google_signin": "Iniciar Sessão com sua conta do Google",
|
||||||
"page.login.oidc_signin": "Iniciar Sessão com sua conta do %s",
|
"page.login.oidc_signin": "Iniciar Sessão com sua conta do OpenID Connect",
|
||||||
"page.login.webauthn_login": "Entrar com senha",
|
"page.login.webauthn_login": "Entrar com senha",
|
||||||
"page.login.webauthn_login.error": "Não é possível fazer login com senha",
|
"page.login.webauthn_login.error": "Não é possível fazer login com senha",
|
||||||
"page.integrations.title": "Integrações",
|
"page.integrations.title": "Integrações",
|
||||||
@ -261,7 +256,6 @@
|
|||||||
"alert.no_bookmark": "Não há favorito neste momento.",
|
"alert.no_bookmark": "Não há favorito neste momento.",
|
||||||
"alert.no_category": "Não há categoria.",
|
"alert.no_category": "Não há categoria.",
|
||||||
"alert.no_category_entry": "Não há itens nesta categoria.",
|
"alert.no_category_entry": "Não há itens nesta categoria.",
|
||||||
"alert.no_tag_entry": "Não há itens que correspondam a esta etiqueta.",
|
|
||||||
"alert.no_feed_entry": "Não há itens nessa fonte.",
|
"alert.no_feed_entry": "Não há itens nessa fonte.",
|
||||||
"alert.no_feed": "Não há inscrições.",
|
"alert.no_feed": "Não há inscrições.",
|
||||||
"alert.no_feed_in_category": "Não há inscrições nessa categoria.",
|
"alert.no_feed_in_category": "Não há inscrições nessa categoria.",
|
||||||
@ -296,14 +290,6 @@
|
|||||||
"error.password_min_length": "A senha deve ter no mínimo 6 caracteres.",
|
"error.password_min_length": "A senha deve ter no mínimo 6 caracteres.",
|
||||||
"error.settings_mandatory_fields": "Os campos de nome de usuário, tema, idioma e fuso horário são obrigatórios.",
|
"error.settings_mandatory_fields": "Os campos de nome de usuário, tema, idioma e fuso horário são obrigatórios.",
|
||||||
"error.settings_reading_speed_is_positive": "As velocidades de leitura devem ser inteiros positivos.",
|
"error.settings_reading_speed_is_positive": "As velocidades de leitura devem ser inteiros positivos.",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.entries_per_page_invalid": "O número de itens por página é inválido.",
|
"error.entries_per_page_invalid": "O número de itens por página é inválido.",
|
||||||
"error.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.",
|
"error.feed_mandatory_fields": "O campo de URL e categoria são obrigatórios.",
|
||||||
"error.feed_already_exists": "Este feed já existe.",
|
"error.feed_already_exists": "Este feed já existe.",
|
||||||
@ -328,7 +314,6 @@
|
|||||||
"form.feed.label.title": "Título",
|
"form.feed.label.title": "Título",
|
||||||
"form.feed.label.site_url": "URL do site",
|
"form.feed.label.site_url": "URL do site",
|
||||||
"form.feed.label.feed_url": "URL da fonte",
|
"form.feed.label.feed_url": "URL da fonte",
|
||||||
"form.feed.label.description": "Descrição",
|
|
||||||
"form.feed.label.category": "Categoria",
|
"form.feed.label.category": "Categoria",
|
||||||
"form.feed.label.crawler": "Obter conteúdo original",
|
"form.feed.label.crawler": "Obter conteúdo original",
|
||||||
"form.feed.label.feed_username": "Nome de usuário da fonte",
|
"form.feed.label.feed_username": "Nome de usuário da fonte",
|
||||||
@ -348,13 +333,6 @@
|
|||||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||||
"form.feed.label.fetch_via_proxy": "Buscar via proxy",
|
"form.feed.label.fetch_via_proxy": "Buscar via proxy",
|
||||||
"form.feed.label.hide_globally": "Ocultar entradas na lista global não lida",
|
"form.feed.label.hide_globally": "Ocultar entradas na lista global não lida",
|
||||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
|
||||||
"form.feed.fieldset.general": "General",
|
"form.feed.fieldset.general": "General",
|
||||||
"form.feed.fieldset.rules": "Rules",
|
"form.feed.fieldset.rules": "Rules",
|
||||||
"form.feed.fieldset.network_settings": "Network Settings",
|
"form.feed.fieldset.network_settings": "Network Settings",
|
||||||
@ -395,18 +373,11 @@
|
|||||||
"form.prefs.label.default_home_page": "Página inicial predefinida",
|
"form.prefs.label.default_home_page": "Página inicial predefinida",
|
||||||
"form.prefs.label.categories_sorting_order": "Classificação das categorias",
|
"form.prefs.label.categories_sorting_order": "Classificação das categorias",
|
||||||
"form.prefs.label.mark_read_on_view": "Marcar automaticamente as entradas como lidas quando visualizadas",
|
"form.prefs.label.mark_read_on_view": "Marcar automaticamente as entradas como lidas quando visualizadas",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
|
||||||
"form.import.label.file": "Arquivo OPML",
|
"form.import.label.file": "Arquivo OPML",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "Save entries to Betula",
|
|
||||||
"form.integration.betula_url": "Betula server URL",
|
|
||||||
"form.integration.betula_token": "Betula Token",
|
|
||||||
"form.integration.fever_activate": "Ativar API do Fever",
|
"form.integration.fever_activate": "Ativar API do Fever",
|
||||||
"form.integration.fever_username": "Nome de usuário do Fever",
|
"form.integration.fever_username": "Nome de usuário do Fever",
|
||||||
"form.integration.fever_password": "Senha do Fever",
|
"form.integration.fever_password": "Senha do Fever",
|
||||||
@ -478,10 +449,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Palavra-passe para utilizador da Matrix",
|
"form.integration.matrix_bot_password": "Palavra-passe para utilizador da Matrix",
|
||||||
"form.integration.matrix_bot_url": "URL do servidor Matrix",
|
"form.integration.matrix_bot_url": "URL do servidor Matrix",
|
||||||
"form.integration.matrix_bot_chat_id": "Identificação da sala Matrix",
|
"form.integration.matrix_bot_chat_id": "Identificação da sala Matrix",
|
||||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "Salvar itens no Readeck",
|
"form.integration.readeck_activate": "Salvar itens no Readeck",
|
||||||
"form.integration.readeck_endpoint": "Endpoint de API do Readeck",
|
"form.integration.readeck_endpoint": "Endpoint de API do Readeck",
|
||||||
"form.integration.readeck_api_key": "Chave de API do Readeck",
|
"form.integration.readeck_api_key": "Chave de API do Readeck",
|
||||||
@ -499,13 +466,6 @@
|
|||||||
"form.integration.webhook_secret": "Webhook Secret",
|
"form.integration.webhook_secret": "Webhook Secret",
|
||||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
|
||||||
"form.api_key.label.description": "Etiqueta da chave de API",
|
"form.api_key.label.description": "Etiqueta da chave de API",
|
||||||
"form.submit.loading": "Carregando...",
|
"form.submit.loading": "Carregando...",
|
||||||
"form.submit.saving": "Salvando...",
|
"form.submit.saving": "Salvando...",
|
||||||
@ -545,7 +505,7 @@
|
|||||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP client error: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
@ -564,16 +524,5 @@
|
|||||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "Velocidade de reprodução do áudio/vídeo",
|
|
||||||
"error.settings_media_playback_rate_range": "A velocidade de reprodução está fora do intervalo",
|
|
||||||
"enclosure_media_controls.seek" : "Seek:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Speed:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Faster",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Slower",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "Отметить всё как прочитанное",
|
"menu.mark_all_as_read": "Отметить всё как прочитанное",
|
||||||
"menu.show_all_entries": "Показать все статьи",
|
"menu.show_all_entries": "Показать все статьи",
|
||||||
"menu.show_only_unread_entries": "Показывать только непрочитанные статьи",
|
"menu.show_only_unread_entries": "Показывать только непрочитанные статьи",
|
||||||
"menu.show_only_starred_entries": "Показывать только избранные статьи",
|
|
||||||
"menu.refresh_feed": "Обновить",
|
"menu.refresh_feed": "Обновить",
|
||||||
"menu.refresh_all_feeds": "Обновить все подписки в фоне",
|
"menu.refresh_all_feeds": "Обновить все подписки в фоне",
|
||||||
"menu.edit_feed": "Изменить",
|
"menu.edit_feed": "Изменить",
|
||||||
@ -56,9 +55,7 @@
|
|||||||
"search.label": "Поиск",
|
"search.label": "Поиск",
|
||||||
"search.placeholder": "Поиск…",
|
"search.placeholder": "Поиск…",
|
||||||
"search.submit": "Search",
|
"search.submit": "Search",
|
||||||
"pagination.last": "Last",
|
|
||||||
"pagination.next": "Следующая",
|
"pagination.next": "Следующая",
|
||||||
"pagination.first": "First",
|
|
||||||
"pagination.previous": "Предыдущая",
|
"pagination.previous": "Предыдущая",
|
||||||
"entry.status.unread": "Не прочитано",
|
"entry.status.unread": "Не прочитано",
|
||||||
"entry.status.read": "Прочитано",
|
"entry.status.read": "Прочитано",
|
||||||
@ -190,8 +187,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "Перейти к подписке",
|
"page.keyboard_shortcuts.go_to_feed": "Перейти к подписке",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "Перейти к предыдущей странице",
|
"page.keyboard_shortcuts.go_to_previous_page": "Перейти к предыдущей странице",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "Перейти к следующей странице",
|
"page.keyboard_shortcuts.go_to_next_page": "Перейти к следующей странице",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "Перейти к нижнему элементу",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "Перейти к верхнему элементу",
|
|
||||||
"page.keyboard_shortcuts.open_item": "Открыть выбранный элемент",
|
"page.keyboard_shortcuts.open_item": "Открыть выбранный элемент",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "Открыть оригинальную ссылку в текущей вкладке",
|
"page.keyboard_shortcuts.open_original_same_window": "Открыть оригинальную ссылку в текущей вкладке",
|
||||||
"page.keyboard_shortcuts.open_original": "Открыть оригинальную ссылку",
|
"page.keyboard_shortcuts.open_original": "Открыть оригинальную ссылку",
|
||||||
@ -220,8 +215,8 @@
|
|||||||
"page.settings.title": "Настройки",
|
"page.settings.title": "Настройки",
|
||||||
"page.settings.link_google_account": "Привязать мой Google аккаунт",
|
"page.settings.link_google_account": "Привязать мой Google аккаунт",
|
||||||
"page.settings.unlink_google_account": "Отвязать мой Google аккаунт",
|
"page.settings.unlink_google_account": "Отвязать мой Google аккаунт",
|
||||||
"page.settings.link_oidc_account": "Привязать мой %s аккаунт",
|
"page.settings.link_oidc_account": "Привязать мой OpenID Connect аккаунт",
|
||||||
"page.settings.unlink_oidc_account": "Отвязать мой %s аккаунт",
|
"page.settings.unlink_oidc_account": "Отвязать мой OpenID Connect аккаунт",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Actions",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||||
@ -236,7 +231,7 @@
|
|||||||
],
|
],
|
||||||
"page.login.title": "Войти",
|
"page.login.title": "Войти",
|
||||||
"page.login.google_signin": "Войти с помощью Google",
|
"page.login.google_signin": "Войти с помощью Google",
|
||||||
"page.login.oidc_signin": "Войти с помощью %s",
|
"page.login.oidc_signin": "Войти с помощью OpenID Connect",
|
||||||
"page.login.webauthn_login": "Войти с паролем",
|
"page.login.webauthn_login": "Войти с паролем",
|
||||||
"page.login.webauthn_login.error": "Невозможно войти с паролем",
|
"page.login.webauthn_login.error": "Невозможно войти с паролем",
|
||||||
"page.integrations.title": "Интеграции",
|
"page.integrations.title": "Интеграции",
|
||||||
@ -271,7 +266,6 @@
|
|||||||
"alert.no_bookmark": "Избранное отсутствует.",
|
"alert.no_bookmark": "Избранное отсутствует.",
|
||||||
"alert.no_category": "Категории отсутствуют.",
|
"alert.no_category": "Категории отсутствуют.",
|
||||||
"alert.no_category_entry": "В этой категории нет статей.",
|
"alert.no_category_entry": "В этой категории нет статей.",
|
||||||
"alert.no_tag_entry": "Нет записей, соответствующих этому тегу.",
|
|
||||||
"alert.no_feed_entry": "В этой подписке отсутствуют статьи.",
|
"alert.no_feed_entry": "В этой подписке отсутствуют статьи.",
|
||||||
"alert.no_feed": "У вас нет ни одной подписки.",
|
"alert.no_feed": "У вас нет ни одной подписки.",
|
||||||
"alert.no_feed_in_category": "Для этой категории нет подписки.",
|
"alert.no_feed_in_category": "Для этой категории нет подписки.",
|
||||||
@ -306,14 +300,6 @@
|
|||||||
"error.password_min_length": "Вы должны использовать минимум 6 символов.",
|
"error.password_min_length": "Вы должны использовать минимум 6 символов.",
|
||||||
"error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.",
|
"error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.",
|
||||||
"error.settings_reading_speed_is_positive": "Скорость чтения должна быть целым положительным числом.",
|
"error.settings_reading_speed_is_positive": "Скорость чтения должна быть целым положительным числом.",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.entries_per_page_invalid": "Недопустимое значение количества записей на странице.",
|
"error.entries_per_page_invalid": "Недопустимое значение количества записей на странице.",
|
||||||
"error.feed_mandatory_fields": "Ссылка и категория обязательны.",
|
"error.feed_mandatory_fields": "Ссылка и категория обязательны.",
|
||||||
"error.feed_already_exists": "Эта подписка уже существует.",
|
"error.feed_already_exists": "Эта подписка уже существует.",
|
||||||
@ -338,7 +324,6 @@
|
|||||||
"form.feed.label.title": "Название",
|
"form.feed.label.title": "Название",
|
||||||
"form.feed.label.site_url": "Адрес сайта",
|
"form.feed.label.site_url": "Адрес сайта",
|
||||||
"form.feed.label.feed_url": "Адрес подписки",
|
"form.feed.label.feed_url": "Адрес подписки",
|
||||||
"form.feed.label.description": "Описание",
|
|
||||||
"form.feed.label.category": "Категория",
|
"form.feed.label.category": "Категория",
|
||||||
"form.feed.label.crawler": "Извлечь оригинальное содержимое",
|
"form.feed.label.crawler": "Извлечь оригинальное содержимое",
|
||||||
"form.feed.label.feed_username": "Имя пользователя подписки",
|
"form.feed.label.feed_username": "Имя пользователя подписки",
|
||||||
@ -358,13 +343,6 @@
|
|||||||
"form.feed.label.disabled": "Не обновлять эту подписку",
|
"form.feed.label.disabled": "Не обновлять эту подписку",
|
||||||
"form.feed.label.no_media_player": "Отключить медиаплеер (аудио и видео)",
|
"form.feed.label.no_media_player": "Отключить медиаплеер (аудио и видео)",
|
||||||
"form.feed.label.hide_globally": "Скрыть записи в глобальном списке непрочитанных",
|
"form.feed.label.hide_globally": "Скрыть записи в глобальном списке непрочитанных",
|
||||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
|
||||||
"form.feed.fieldset.general": "General",
|
"form.feed.fieldset.general": "General",
|
||||||
"form.feed.fieldset.rules": "Rules",
|
"form.feed.fieldset.rules": "Rules",
|
||||||
"form.feed.fieldset.network_settings": "Network Settings",
|
"form.feed.fieldset.network_settings": "Network Settings",
|
||||||
@ -405,18 +383,11 @@
|
|||||||
"form.prefs.label.default_home_page": "Домашняя страница по умолчанию",
|
"form.prefs.label.default_home_page": "Домашняя страница по умолчанию",
|
||||||
"form.prefs.label.categories_sorting_order": "Сортировка категорий",
|
"form.prefs.label.categories_sorting_order": "Сортировка категорий",
|
||||||
"form.prefs.label.mark_read_on_view": "Автоматически отмечать записи как прочитанные при просмотре",
|
"form.prefs.label.mark_read_on_view": "Автоматически отмечать записи как прочитанные при просмотре",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
|
||||||
"form.import.label.file": "OPML файл",
|
"form.import.label.file": "OPML файл",
|
||||||
"form.import.label.url": "Ссылка",
|
"form.import.label.url": "Ссылка",
|
||||||
"form.integration.betula_activate": "Сохранять статьи в Бетулу",
|
|
||||||
"form.integration.betula_url": "Адрес сервера Бетулы",
|
|
||||||
"form.integration.betula_token": "Токен Бетулы",
|
|
||||||
"form.integration.fever_activate": "Активировать Fever API",
|
"form.integration.fever_activate": "Активировать Fever API",
|
||||||
"form.integration.fever_username": "Имя пользователя Fever",
|
"form.integration.fever_username": "Имя пользователя Fever",
|
||||||
"form.integration.fever_password": "Пароль Fever",
|
"form.integration.fever_password": "Пароль Fever",
|
||||||
@ -488,10 +459,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Пароль пользователя Matrix",
|
"form.integration.matrix_bot_password": "Пароль пользователя Matrix",
|
||||||
"form.integration.matrix_bot_url": "Ссылка на сервер Matrix",
|
"form.integration.matrix_bot_url": "Ссылка на сервер Matrix",
|
||||||
"form.integration.matrix_bot_chat_id": "ID комнаты Matrix",
|
"form.integration.matrix_bot_chat_id": "ID комнаты Matrix",
|
||||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "Сохранять статьи в Readeck",
|
"form.integration.readeck_activate": "Сохранять статьи в Readeck",
|
||||||
"form.integration.readeck_endpoint": "Конечная точка Readeck API",
|
"form.integration.readeck_endpoint": "Конечная точка Readeck API",
|
||||||
"form.integration.readeck_api_key": "API-ключ Readeck",
|
"form.integration.readeck_api_key": "API-ключ Readeck",
|
||||||
@ -509,13 +476,6 @@
|
|||||||
"form.integration.webhook_secret": "Секретный ключ для вебхуков",
|
"form.integration.webhook_secret": "Секретный ключ для вебхуков",
|
||||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
|
||||||
"form.api_key.label.description": "Описание API-ключа",
|
"form.api_key.label.description": "Описание API-ключа",
|
||||||
"form.submit.loading": "Загрузка…",
|
"form.submit.loading": "Загрузка…",
|
||||||
"form.submit.saving": "Сохранение…",
|
"form.submit.saving": "Сохранение…",
|
||||||
@ -562,7 +522,7 @@
|
|||||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP client error: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
@ -581,16 +541,5 @@
|
|||||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "Скорость воспроизведения аудио/видео",
|
|
||||||
"error.settings_media_playback_rate_range": "Скорость воспроизведения выходит за пределы диапазона",
|
|
||||||
"enclosure_media_controls.seek" : "Seek:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Speed:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Faster",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Slower",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -19,8 +19,8 @@
|
|||||||
"action.home_screen": "Додати до головного екрану",
|
"action.home_screen": "Додати до головного екрану",
|
||||||
"tooltip.keyboard_shortcuts": "Комбінація клавіш: %s",
|
"tooltip.keyboard_shortcuts": "Комбінація клавіш: %s",
|
||||||
"tooltip.logged_user": "Здійснено вхід як %s",
|
"tooltip.logged_user": "Здійснено вхід як %s",
|
||||||
"menu.title": "Меню",
|
"menu.title": "Menu",
|
||||||
"menu.home_page": "Головна сторінка",
|
"menu.home_page": "Home page",
|
||||||
"menu.unread": "Непрочитане",
|
"menu.unread": "Непрочитане",
|
||||||
"menu.starred": "З зірочкою",
|
"menu.starred": "З зірочкою",
|
||||||
"menu.history": "Історія",
|
"menu.history": "Історія",
|
||||||
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "Відмітити все як прочитане",
|
"menu.mark_all_as_read": "Відмітити все як прочитане",
|
||||||
"menu.show_all_entries": "Показати всі записи",
|
"menu.show_all_entries": "Показати всі записи",
|
||||||
"menu.show_only_unread_entries": "Показати тільки непрочитані записи",
|
"menu.show_only_unread_entries": "Показати тільки непрочитані записи",
|
||||||
"menu.show_only_starred_entries": "Показати тільки записи з зірочкою",
|
|
||||||
"menu.refresh_feed": "Оновити",
|
"menu.refresh_feed": "Оновити",
|
||||||
"menu.refresh_all_feeds": "Оновити всі стрічки у фоновому режимі",
|
"menu.refresh_all_feeds": "Оновити всі стрічки у фоновому режимі",
|
||||||
"menu.edit_feed": "Редагувати",
|
"menu.edit_feed": "Редагувати",
|
||||||
@ -55,11 +54,9 @@
|
|||||||
"menu.shared_entries": "Спільні записи",
|
"menu.shared_entries": "Спільні записи",
|
||||||
"search.label": "Пошук",
|
"search.label": "Пошук",
|
||||||
"search.placeholder": "Шукати...",
|
"search.placeholder": "Шукати...",
|
||||||
"search.submit": "Знайти",
|
"search.submit": "Search",
|
||||||
"pagination.last": "Остання",
|
"pagination.next": "Вперед",
|
||||||
"pagination.next": "Наступна",
|
"pagination.previous": "Назад",
|
||||||
"pagination.first": "Перша",
|
|
||||||
"pagination.previous": "Попередня",
|
|
||||||
"entry.status.unread": "Непрочитане",
|
"entry.status.unread": "Непрочитане",
|
||||||
"entry.status.read": "Прочитане",
|
"entry.status.read": "Прочитане",
|
||||||
"entry.status.toast.unread": "Відмічено непрочитаним",
|
"entry.status.toast.unread": "Відмічено непрочитаним",
|
||||||
@ -92,7 +89,7 @@
|
|||||||
"читати %d хвилин"
|
"читати %d хвилин"
|
||||||
],
|
],
|
||||||
"entry.tags.label": "Теги:",
|
"entry.tags.label": "Теги:",
|
||||||
"page.shared_entries.title": "Спільні записи",
|
"page.shared_entries.title": "Спильні записи",
|
||||||
"page.shared_entries_count": [
|
"page.shared_entries_count": [
|
||||||
"%d shared entry",
|
"%d shared entry",
|
||||||
"%d shared entries",
|
"%d shared entries",
|
||||||
@ -134,9 +131,9 @@
|
|||||||
"page.edit_category.title": "Редагування категорії: %s",
|
"page.edit_category.title": "Редагування категорії: %s",
|
||||||
"page.edit_user.title": "Редагування користувача: %s",
|
"page.edit_user.title": "Редагування користувача: %s",
|
||||||
"page.feeds.title": "Стрічки",
|
"page.feeds.title": "Стрічки",
|
||||||
"page.category_label": "Категорія: %s",
|
"page.category_label": "Category: %s",
|
||||||
"page.feeds.last_check": "Остання перевірка:",
|
"page.feeds.last_check": "Остання перевірка:",
|
||||||
"page.feeds.next_check": "Наступна перевірка:",
|
"page.feeds.next_check": "Next check:",
|
||||||
"page.feeds.read_counter": "Кількість прочитаних записів",
|
"page.feeds.read_counter": "Кількість прочитаних записів",
|
||||||
"page.feeds.error_count": [
|
"page.feeds.error_count": [
|
||||||
"%d помилка",
|
"%d помилка",
|
||||||
@ -190,8 +187,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "Перейти до стрічки",
|
"page.keyboard_shortcuts.go_to_feed": "Перейти до стрічки",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "Перейти до попередньої сторінки",
|
"page.keyboard_shortcuts.go_to_previous_page": "Перейти до попередньої сторінки",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "Перейти до наступної сторінки",
|
"page.keyboard_shortcuts.go_to_next_page": "Перейти до наступної сторінки",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "Перейти до нижнього пункту",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "Перейти до верхнього пункту",
|
|
||||||
"page.keyboard_shortcuts.open_item": "Відкрити виділений запис",
|
"page.keyboard_shortcuts.open_item": "Відкрити виділений запис",
|
||||||
"page.keyboard_shortcuts.open_original": "Відкрити оригінальне посилання",
|
"page.keyboard_shortcuts.open_original": "Відкрити оригінальне посилання",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "Відкрити оригінальне посилання в поточній вкладці",
|
"page.keyboard_shortcuts.open_original_same_window": "Відкрити оригінальне посилання в поточній вкладці",
|
||||||
@ -220,8 +215,8 @@
|
|||||||
"page.settings.title": "Налаштування ",
|
"page.settings.title": "Налаштування ",
|
||||||
"page.settings.link_google_account": "Підключити мій обліковий запис Google",
|
"page.settings.link_google_account": "Підключити мій обліковий запис Google",
|
||||||
"page.settings.unlink_google_account": "Відключити мій обліковий запис Google",
|
"page.settings.unlink_google_account": "Відключити мій обліковий запис Google",
|
||||||
"page.settings.link_oidc_account": "Підключити мій обліковий запис %s",
|
"page.settings.link_oidc_account": "Підключити мій обліковий запис OpenID Connect",
|
||||||
"page.settings.unlink_oidc_account": "Відключити мій обліковий запис %s",
|
"page.settings.unlink_oidc_account": "Відключити мій обліковий запис OpenID Connect",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "Actions",
|
"page.settings.webauthn.actions": "Actions",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey Name",
|
"page.settings.webauthn.passkey_name": "Passkey Name",
|
||||||
@ -236,7 +231,7 @@
|
|||||||
],
|
],
|
||||||
"page.login.title": "Вхід",
|
"page.login.title": "Вхід",
|
||||||
"page.login.google_signin": "Увійти через Google",
|
"page.login.google_signin": "Увійти через Google",
|
||||||
"page.login.oidc_signin": "Увійти через %s",
|
"page.login.oidc_signin": "Увійти через OpenID Connect",
|
||||||
"page.login.webauthn_login": "Увійти за допомогою пароля",
|
"page.login.webauthn_login": "Увійти за допомогою пароля",
|
||||||
"page.login.webauthn_login.error": "Неможливо ввійти за допомогою ключа доступу",
|
"page.login.webauthn_login.error": "Неможливо ввійти за допомогою ключа доступу",
|
||||||
"page.integrations.title": "Інтеграції",
|
"page.integrations.title": "Інтеграції",
|
||||||
@ -271,7 +266,6 @@
|
|||||||
"alert.no_bookmark": "Наразі закладки відсутні.",
|
"alert.no_bookmark": "Наразі закладки відсутні.",
|
||||||
"alert.no_category": "Немає категорії.",
|
"alert.no_category": "Немає категорії.",
|
||||||
"alert.no_category_entry": "У цій категорії немає записів.",
|
"alert.no_category_entry": "У цій категорії немає записів.",
|
||||||
"alert.no_tag_entry": "Немає записів, що відповідають цьому тегу.",
|
|
||||||
"alert.no_feed_entry": "У цій стрічці немає записів.",
|
"alert.no_feed_entry": "У цій стрічці немає записів.",
|
||||||
"alert.no_feed": "У вас немає підписок.",
|
"alert.no_feed": "У вас немає підписок.",
|
||||||
"alert.no_feed_in_category": "У цій категорії немає підписок.",
|
"alert.no_feed_in_category": "У цій категорії немає підписок.",
|
||||||
@ -313,14 +307,6 @@
|
|||||||
"error.password_min_length": "Пароль має складати щонайменше 6 символів.",
|
"error.password_min_length": "Пароль має складати щонайменше 6 символів.",
|
||||||
"error.settings_mandatory_fields": "Поля імені, теми, мови та часового поясу є обов’язковими.",
|
"error.settings_mandatory_fields": "Поля імені, теми, мови та часового поясу є обов’язковими.",
|
||||||
"error.settings_reading_speed_is_positive": "Швидкість читання має бути додатнім цілим числом.",
|
"error.settings_reading_speed_is_positive": "Швидкість читання має бути додатнім цілим числом.",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.entries_per_page_invalid": "Число записів на сторінку недійсне.",
|
"error.entries_per_page_invalid": "Число записів на сторінку недійсне.",
|
||||||
"error.feed_mandatory_fields": "URL та категорія є обов’язковими.",
|
"error.feed_mandatory_fields": "URL та категорія є обов’язковими.",
|
||||||
"error.feed_already_exists": "Така стрічка вже існує.",
|
"error.feed_already_exists": "Така стрічка вже існує.",
|
||||||
@ -338,7 +324,6 @@
|
|||||||
"form.feed.label.title": "Назва",
|
"form.feed.label.title": "Назва",
|
||||||
"form.feed.label.site_url": "URL-адреса сайту",
|
"form.feed.label.site_url": "URL-адреса сайту",
|
||||||
"form.feed.label.feed_url": "URL-адреса стрічки",
|
"form.feed.label.feed_url": "URL-адреса стрічки",
|
||||||
"form.feed.label.description": "Опис",
|
|
||||||
"form.feed.label.category": "Категорія",
|
"form.feed.label.category": "Категорія",
|
||||||
"form.feed.label.crawler": "Завантажувати оригінальний вміст",
|
"form.feed.label.crawler": "Завантажувати оригінальний вміст",
|
||||||
"form.feed.label.feed_username": "Ім’я користувача для завантаження",
|
"form.feed.label.feed_username": "Ім’я користувача для завантаження",
|
||||||
@ -358,13 +343,6 @@
|
|||||||
"form.feed.label.disabled": "Не оновлювати цю стрічку",
|
"form.feed.label.disabled": "Не оновлювати цю стрічку",
|
||||||
"form.feed.label.no_media_player": "No media player (audio/video)",
|
"form.feed.label.no_media_player": "No media player (audio/video)",
|
||||||
"form.feed.label.hide_globally": "Приховати записи в глобальному списку непрочитаного",
|
"form.feed.label.hide_globally": "Приховати записи в глобальному списку непрочитаного",
|
||||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
|
||||||
"form.category.label.title": "Назва",
|
"form.category.label.title": "Назва",
|
||||||
"form.category.hide_globally": "Приховати записи в глобальному списку непрочитаного",
|
"form.category.hide_globally": "Приховати записи в глобальному списку непрочитаного",
|
||||||
"form.feed.fieldset.general": "General",
|
"form.feed.fieldset.general": "General",
|
||||||
@ -405,18 +383,11 @@
|
|||||||
"form.prefs.label.default_home_page": "Домашня сторінка за умовчанням",
|
"form.prefs.label.default_home_page": "Домашня сторінка за умовчанням",
|
||||||
"form.prefs.label.categories_sorting_order": "Сортування за категоріями",
|
"form.prefs.label.categories_sorting_order": "Сортування за категоріями",
|
||||||
"form.prefs.label.mark_read_on_view": "Автоматично позначати записи як прочитані під час перегляду",
|
"form.prefs.label.mark_read_on_view": "Автоматично позначати записи як прочитані під час перегляду",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
|
||||||
"form.prefs.fieldset.application_settings": "Application Settings",
|
"form.prefs.fieldset.application_settings": "Application Settings",
|
||||||
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
"form.prefs.fieldset.authentication_settings": "Authentication Settings",
|
||||||
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
"form.prefs.fieldset.reader_settings": "Reader Settings",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
|
||||||
"form.import.label.file": "Файл OPML",
|
"form.import.label.file": "Файл OPML",
|
||||||
"form.import.label.url": "URL-адреса",
|
"form.import.label.url": "URL-адреса",
|
||||||
"form.integration.betula_activate": "Save entries to Betula",
|
|
||||||
"form.integration.betula_url": "Betula server URL",
|
|
||||||
"form.integration.betula_token": "Betula Token",
|
|
||||||
"form.integration.fever_activate": "Увімкнути API Fever",
|
"form.integration.fever_activate": "Увімкнути API Fever",
|
||||||
"form.integration.fever_username": "Ім’я користувача Fever",
|
"form.integration.fever_username": "Ім’я користувача Fever",
|
||||||
"form.integration.fever_password": "Пароль Fever",
|
"form.integration.fever_password": "Пароль Fever",
|
||||||
@ -488,10 +459,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Пароль для користувача Matrix",
|
"form.integration.matrix_bot_password": "Пароль для користувача Matrix",
|
||||||
"form.integration.matrix_bot_url": "URL-адреса сервера Матриці",
|
"form.integration.matrix_bot_url": "URL-адреса сервера Матриці",
|
||||||
"form.integration.matrix_bot_chat_id": "Ідентифікатор кімнати Матриці",
|
"form.integration.matrix_bot_chat_id": "Ідентифікатор кімнати Матриці",
|
||||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "Зберігати статті до Readeck",
|
"form.integration.readeck_activate": "Зберігати статті до Readeck",
|
||||||
"form.integration.readeck_endpoint": "Readeck API Endpoint",
|
"form.integration.readeck_endpoint": "Readeck API Endpoint",
|
||||||
"form.integration.readeck_api_key": "Ключ API Readeck",
|
"form.integration.readeck_api_key": "Ключ API Readeck",
|
||||||
@ -509,13 +476,6 @@
|
|||||||
"form.integration.webhook_secret": "Webhook Secret",
|
"form.integration.webhook_secret": "Webhook Secret",
|
||||||
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
"form.integration.rssbridge_url": "RSS-Bridge server URL",
|
||||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
|
||||||
"form.api_key.label.description": "Назва ключа API",
|
"form.api_key.label.description": "Назва ключа API",
|
||||||
"form.submit.loading": "Завантаження...",
|
"form.submit.loading": "Завантаження...",
|
||||||
"form.submit.saving": "Зберігаю...",
|
"form.submit.saving": "Зберігаю...",
|
||||||
@ -562,7 +522,7 @@
|
|||||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP client error: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
@ -581,16 +541,5 @@
|
|||||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "Швидкість відтворення аудіо/відео",
|
|
||||||
"error.settings_media_playback_rate_range": "Швидкість відтворення виходить за межі діапазону",
|
|
||||||
"enclosure_media_controls.seek" : "Seek:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Speed:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Faster",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Slower",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"skip_to_content": "跳转至内容",
|
"skip_to_content": "Skip to content",
|
||||||
"confirm.question": "您确认吗?",
|
"confirm.question": "您确认吗?",
|
||||||
"confirm.question.refresh": "您是否要强制刷新?",
|
"confirm.question.refresh": "您是否要强制刷新?",
|
||||||
"confirm.yes": "是",
|
"confirm.yes": "是",
|
||||||
@ -19,8 +19,8 @@
|
|||||||
"action.home_screen": "添加到主屏幕",
|
"action.home_screen": "添加到主屏幕",
|
||||||
"tooltip.keyboard_shortcuts": "快捷键: %s",
|
"tooltip.keyboard_shortcuts": "快捷键: %s",
|
||||||
"tooltip.logged_user": "当前登录 %s",
|
"tooltip.logged_user": "当前登录 %s",
|
||||||
"menu.title": "菜单",
|
"menu.title": "Menu",
|
||||||
"menu.home_page": "首页",
|
"menu.home_page": "Home page",
|
||||||
"menu.unread": "未读",
|
"menu.unread": "未读",
|
||||||
"menu.starred": "收藏",
|
"menu.starred": "收藏",
|
||||||
"menu.history": "历史",
|
"menu.history": "历史",
|
||||||
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "全部标为已读",
|
"menu.mark_all_as_read": "全部标为已读",
|
||||||
"menu.show_all_entries": "显示所有文章",
|
"menu.show_all_entries": "显示所有文章",
|
||||||
"menu.show_only_unread_entries": "仅显示未读文章",
|
"menu.show_only_unread_entries": "仅显示未读文章",
|
||||||
"menu.show_only_starred_entries": "仅显示已收藏文章",
|
|
||||||
"menu.refresh_feed": "更新",
|
"menu.refresh_feed": "更新",
|
||||||
"menu.refresh_all_feeds": "在后台更新全部源",
|
"menu.refresh_all_feeds": "在后台更新全部源",
|
||||||
"menu.edit_feed": "编辑",
|
"menu.edit_feed": "编辑",
|
||||||
@ -55,10 +54,8 @@
|
|||||||
"menu.shared_entries": "已分享的文章",
|
"menu.shared_entries": "已分享的文章",
|
||||||
"search.label": "搜索",
|
"search.label": "搜索",
|
||||||
"search.placeholder": "搜索…",
|
"search.placeholder": "搜索…",
|
||||||
"search.submit": "查找",
|
"search.submit": "Search",
|
||||||
"pagination.last": "最后一页",
|
|
||||||
"pagination.next": "下一页",
|
"pagination.next": "下一页",
|
||||||
"pagination.first": "第一页",
|
|
||||||
"pagination.previous": "上一页",
|
"pagination.previous": "上一页",
|
||||||
"entry.status.unread": "标为未读",
|
"entry.status.unread": "标为未读",
|
||||||
"entry.status.read": "标为已读",
|
"entry.status.read": "标为已读",
|
||||||
@ -92,18 +89,18 @@
|
|||||||
"entry.tags.label": "标签:",
|
"entry.tags.label": "标签:",
|
||||||
"page.shared_entries.title": "已分享的文章",
|
"page.shared_entries.title": "已分享的文章",
|
||||||
"page.shared_entries_count": [
|
"page.shared_entries_count": [
|
||||||
"%d 已分享的文章"
|
"%d shared entry"
|
||||||
],
|
],
|
||||||
"page.unread.title": "未读",
|
"page.unread.title": "未读",
|
||||||
"page.unread_entry_count": [
|
"page.unread_entry_count": [
|
||||||
"%d 未读的文章"
|
"%d unread entry"
|
||||||
],
|
],
|
||||||
"page.total_entry_count": [
|
"page.total_entry_count": [
|
||||||
"%d 文章总数"
|
"%d entry in total"
|
||||||
],
|
],
|
||||||
"page.starred.title": "收藏",
|
"page.starred.title": "收藏",
|
||||||
"page.starred_entry_count": [
|
"page.starred_entry_count": [
|
||||||
"%d 收藏的文章"
|
"%d starred entry"
|
||||||
],
|
],
|
||||||
"page.categories.title": "分类",
|
"page.categories.title": "分类",
|
||||||
"page.categories.no_feed": "没有源",
|
"page.categories.no_feed": "没有源",
|
||||||
@ -113,14 +110,14 @@
|
|||||||
"有 %d 个源"
|
"有 %d 个源"
|
||||||
],
|
],
|
||||||
"page.categories_count": [
|
"page.categories_count": [
|
||||||
"%d 分类"
|
"%d category"
|
||||||
],
|
],
|
||||||
"page.new_category.title": "新分类",
|
"page.new_category.title": "新分类",
|
||||||
"page.new_user.title": "新用户",
|
"page.new_user.title": "新用户",
|
||||||
"page.edit_category.title": "编辑分类 : %s",
|
"page.edit_category.title": "编辑分类 : %s",
|
||||||
"page.edit_user.title": "编辑用户 : %s",
|
"page.edit_user.title": "编辑用户 : %s",
|
||||||
"page.feeds.title": "源",
|
"page.feeds.title": "源",
|
||||||
"page.category_label": "分类: %s",
|
"page.category_label": "Category: %s",
|
||||||
"page.feeds.last_check": "最后检查时间:",
|
"page.feeds.last_check": "最后检查时间:",
|
||||||
"page.feeds.next_check": "下次检查时间:",
|
"page.feeds.next_check": "下次检查时间:",
|
||||||
"page.feeds.read_counter": "已读文章数",
|
"page.feeds.read_counter": "已读文章数",
|
||||||
@ -129,7 +126,7 @@
|
|||||||
],
|
],
|
||||||
"page.history.title": "历史",
|
"page.history.title": "历史",
|
||||||
"page.read_entry_count": [
|
"page.read_entry_count": [
|
||||||
"%d 阅读文章"
|
"%d read entry"
|
||||||
],
|
],
|
||||||
"page.import.title": "导入",
|
"page.import.title": "导入",
|
||||||
"page.search.title": "搜索结果",
|
"page.search.title": "搜索结果",
|
||||||
@ -172,8 +169,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "转到源页面",
|
"page.keyboard_shortcuts.go_to_feed": "转到源页面",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "上一页",
|
"page.keyboard_shortcuts.go_to_previous_page": "上一页",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "下一页",
|
"page.keyboard_shortcuts.go_to_next_page": "下一页",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "转到底部项目",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "转到顶部项目",
|
|
||||||
"page.keyboard_shortcuts.open_item": "打开选定的文章",
|
"page.keyboard_shortcuts.open_item": "打开选定的文章",
|
||||||
"page.keyboard_shortcuts.open_original": "打开原始链接",
|
"page.keyboard_shortcuts.open_original": "打开原始链接",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "在当前标签页中打开原始链接",
|
"page.keyboard_shortcuts.open_original_same_window": "在当前标签页中打开原始链接",
|
||||||
@ -202,8 +197,8 @@
|
|||||||
"page.settings.title": "设置",
|
"page.settings.title": "设置",
|
||||||
"page.settings.link_google_account": "关联我的 Google 账户",
|
"page.settings.link_google_account": "关联我的 Google 账户",
|
||||||
"page.settings.unlink_google_account": "解除 Google 账号关联",
|
"page.settings.unlink_google_account": "解除 Google 账号关联",
|
||||||
"page.settings.link_oidc_account": "关联我的 %s 账户",
|
"page.settings.link_oidc_account": "关联我的 OpenID Connect 账户",
|
||||||
"page.settings.unlink_oidc_account": "解除 %s 账号关联",
|
"page.settings.unlink_oidc_account": "解除 OpenID Connect 账号关联",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "操作",
|
"page.settings.webauthn.actions": "操作",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey 名称",
|
"page.settings.webauthn.passkey_name": "Passkey 名称",
|
||||||
@ -216,7 +211,7 @@
|
|||||||
],
|
],
|
||||||
"page.login.title": "登录",
|
"page.login.title": "登录",
|
||||||
"page.login.google_signin": "使用 Google 登录",
|
"page.login.google_signin": "使用 Google 登录",
|
||||||
"page.login.oidc_signin": "使用 %s 登录",
|
"page.login.oidc_signin": "使用 OpenID Connect 登录",
|
||||||
"page.login.webauthn_login": "使用密码登录",
|
"page.login.webauthn_login": "使用密码登录",
|
||||||
"page.login.webauthn_login.error": "无法使用密码登录",
|
"page.login.webauthn_login.error": "无法使用密码登录",
|
||||||
"page.integrations.title": "集成",
|
"page.integrations.title": "集成",
|
||||||
@ -251,7 +246,6 @@
|
|||||||
"alert.no_bookmark": "目前没有收藏",
|
"alert.no_bookmark": "目前没有收藏",
|
||||||
"alert.no_category": "目前没有分类",
|
"alert.no_category": "目前没有分类",
|
||||||
"alert.no_category_entry": "该分类下没有文章",
|
"alert.no_category_entry": "该分类下没有文章",
|
||||||
"alert.no_tag_entry": "没有与此标签匹配的条目。",
|
|
||||||
"alert.no_feed_entry": "该源中没有文章",
|
"alert.no_feed_entry": "该源中没有文章",
|
||||||
"alert.no_feed": "目前没有源",
|
"alert.no_feed": "目前没有源",
|
||||||
"alert.no_history": "目前没有历史",
|
"alert.no_history": "目前没有历史",
|
||||||
@ -294,14 +288,6 @@
|
|||||||
"error.site_url_not_empty": "源网站的网址不能为空。",
|
"error.site_url_not_empty": "源网站的网址不能为空。",
|
||||||
"error.feed_title_not_empty": "订阅源的标题不能为空。",
|
"error.feed_title_not_empty": "订阅源的标题不能为空。",
|
||||||
"error.settings_reading_speed_is_positive": "阅读速度必须是正整数。",
|
"error.settings_reading_speed_is_positive": "阅读速度必须是正整数。",
|
||||||
"error.settings_block_rule_fieldname_invalid": "无效的阻止规则: 规则 #%d 缺少合法的字段名 (可选: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "无效的阻止规则: 规则 #%d 的模式字符必须用‘=’分开。",
|
|
||||||
"error.settings_block_rule_regex_required": "无效的阻止规则: 规则 #%d 的模式字符没有提供。",
|
|
||||||
"error.settings_block_rule_invalid_regex": "无效的阻止规则: 规则 #%d 的模式字符不是合法的正则表达式。",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "无效的保留规则: 规则 #%d 缺少合法的字段名 (可选: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "无效的保留规则: 规则 #%d 的模式字符必须用‘=’分开。",
|
|
||||||
"error.settings_keep_rule_regex_required": "无效的保留规则: 规则 #%d 的模式字符没有提供。",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "无效的保留规则: 规则 #%d 的模式字符不是合法的正则表达式。",
|
|
||||||
"error.feed_category_not_found": "此类别不存在或不属于该用户。",
|
"error.feed_category_not_found": "此类别不存在或不属于该用户。",
|
||||||
"error.feed_invalid_blocklist_rule": "阻止列表规则无效。",
|
"error.feed_invalid_blocklist_rule": "阻止列表规则无效。",
|
||||||
"error.feed_invalid_keeplist_rule": "保留列表规则无效。",
|
"error.feed_invalid_keeplist_rule": "保留列表规则无效。",
|
||||||
@ -318,7 +304,6 @@
|
|||||||
"form.feed.label.title": "标题",
|
"form.feed.label.title": "标题",
|
||||||
"form.feed.label.site_url": "源网站 URL",
|
"form.feed.label.site_url": "源网站 URL",
|
||||||
"form.feed.label.feed_url": "订阅源 URL",
|
"form.feed.label.feed_url": "订阅源 URL",
|
||||||
"form.feed.label.description": "描述",
|
|
||||||
"form.feed.label.category": "类别",
|
"form.feed.label.category": "类别",
|
||||||
"form.feed.label.crawler": "抓取全文内容",
|
"form.feed.label.crawler": "抓取全文内容",
|
||||||
"form.feed.label.feed_username": "源用户名",
|
"form.feed.label.feed_username": "源用户名",
|
||||||
@ -333,18 +318,11 @@
|
|||||||
"form.feed.label.apprise_service_urls": "使用逗号分隔的 Apprise 服务 URL 列表",
|
"form.feed.label.apprise_service_urls": "使用逗号分隔的 Apprise 服务 URL 列表",
|
||||||
"form.feed.label.ignore_http_cache": "忽略 HTTP 缓存",
|
"form.feed.label.ignore_http_cache": "忽略 HTTP 缓存",
|
||||||
"form.feed.label.allow_self_signed_certificates": "允许自签名证书或无效证书",
|
"form.feed.label.allow_self_signed_certificates": "允许自签名证书或无效证书",
|
||||||
"form.feed.label.disable_http2": "关闭 HTTP/2 避免记录指纹",
|
"form.feed.label.disable_http2": "Disable HTTP/2 to avoid fingerprinting",
|
||||||
"form.feed.label.fetch_via_proxy": "通过代理获取",
|
"form.feed.label.fetch_via_proxy": "通过代理获取",
|
||||||
"form.feed.label.disabled": "请勿刷新此源",
|
"form.feed.label.disabled": "请勿刷新此源",
|
||||||
"form.feed.label.no_media_player": "没有媒体播放器(音频/视频)",
|
"form.feed.label.no_media_player": "没有媒体播放器(音频/视频)",
|
||||||
"form.feed.label.hide_globally": "隐藏全局未读列表中的文章",
|
"form.feed.label.hide_globally": "隐藏全局未读列表中的文章",
|
||||||
"form.feed.label.ntfy_activate": "推送条目到ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy优先级",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy最高优先级",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy高优先级",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy默认优先级",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy低优先级",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy最低优先级",
|
|
||||||
"form.feed.fieldset.general": "通用",
|
"form.feed.fieldset.general": "通用",
|
||||||
"form.feed.fieldset.rules": "规则",
|
"form.feed.fieldset.rules": "规则",
|
||||||
"form.feed.fieldset.network_settings": "网络设置",
|
"form.feed.fieldset.network_settings": "网络设置",
|
||||||
@ -385,18 +363,11 @@
|
|||||||
"form.prefs.label.default_home_page": "默认主页",
|
"form.prefs.label.default_home_page": "默认主页",
|
||||||
"form.prefs.label.categories_sorting_order": "分类排序",
|
"form.prefs.label.categories_sorting_order": "分类排序",
|
||||||
"form.prefs.label.mark_read_on_view": "查看时自动将条目标记为已读",
|
"form.prefs.label.mark_read_on_view": "查看时自动将条目标记为已读",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "当浏览时标记条目为已读。对于音频/视频,当播放完成90%%时标记为已读",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "仅当音频/视频播放完成90%%时标记为已读",
|
|
||||||
"form.prefs.label.mark_read_manually": "手动标记条目为已读",
|
|
||||||
"form.prefs.fieldset.application_settings": "应用设置",
|
"form.prefs.fieldset.application_settings": "应用设置",
|
||||||
"form.prefs.fieldset.authentication_settings": "用户认证设置",
|
"form.prefs.fieldset.authentication_settings": "用户认证设置",
|
||||||
"form.prefs.fieldset.reader_settings": "阅读器设置",
|
"form.prefs.fieldset.reader_settings": "阅读器设置",
|
||||||
"form.prefs.fieldset.global_feed_settings": "全局订阅源设置",
|
|
||||||
"form.import.label.file": "OPML 文件",
|
"form.import.label.file": "OPML 文件",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "保存文章到 Betula",
|
|
||||||
"form.integration.betula_url": "Betula 服务地址",
|
|
||||||
"form.integration.betula_token": "Betula 密钥",
|
|
||||||
"form.integration.fever_activate": "启用 Fever API",
|
"form.integration.fever_activate": "启用 Fever API",
|
||||||
"form.integration.fever_username": "Fever 用户名",
|
"form.integration.fever_username": "Fever 用户名",
|
||||||
"form.integration.fever_password": "Fever 密码",
|
"form.integration.fever_password": "Fever 密码",
|
||||||
@ -406,7 +377,7 @@
|
|||||||
"form.integration.googlereader_password": "Google Reader 密码",
|
"form.integration.googlereader_password": "Google Reader 密码",
|
||||||
"form.integration.googlereader_endpoint": "Google Reader API 端点:",
|
"form.integration.googlereader_endpoint": "Google Reader API 端点:",
|
||||||
"form.integration.pinboard_activate": "保存文章到 Pinboard",
|
"form.integration.pinboard_activate": "保存文章到 Pinboard",
|
||||||
"form.integration.pinboard_token": "Pinboard API 密钥",
|
"form.integration.pinboard_token": "Pinboard API Token",
|
||||||
"form.integration.pinboard_tags": "Pinboard 标签",
|
"form.integration.pinboard_tags": "Pinboard 标签",
|
||||||
"form.integration.pinboard_bookmark": "标记为未读",
|
"form.integration.pinboard_bookmark": "标记为未读",
|
||||||
"form.integration.instapaper_activate": "保存文章到 Instapaper",
|
"form.integration.instapaper_activate": "保存文章到 Instapaper",
|
||||||
@ -420,12 +391,12 @@
|
|||||||
"form.integration.wallabag_only_url": "仅发送 URL(而不是完整内容)",
|
"form.integration.wallabag_only_url": "仅发送 URL(而不是完整内容)",
|
||||||
"form.integration.wallabag_endpoint": "Wallabag URL",
|
"form.integration.wallabag_endpoint": "Wallabag URL",
|
||||||
"form.integration.wallabag_client_id": "Wallabag 客户端 ID",
|
"form.integration.wallabag_client_id": "Wallabag 客户端 ID",
|
||||||
"form.integration.wallabag_client_secret": "Wallabag 客户端 密钥",
|
"form.integration.wallabag_client_secret": "Wallabag 客户端 Secret",
|
||||||
"form.integration.wallabag_username": "Wallabag 用户名",
|
"form.integration.wallabag_username": "Wallabag 用户名",
|
||||||
"form.integration.wallabag_password": "Wallabag 密码",
|
"form.integration.wallabag_password": "Wallabag 密码",
|
||||||
"form.integration.notion_activate": "保存文章到 Notion",
|
"form.integration.notion_activate": "保存文章到 Notion",
|
||||||
"form.integration.notion_page_id": "Notion 页面ID",
|
"form.integration.notion_page_id": "Notion Page ID",
|
||||||
"form.integration.notion_token": "Notion 密钥",
|
"form.integration.notion_token": "Notion Secret Token",
|
||||||
"form.integration.apprise_activate": "将新文章推送到 Apprise",
|
"form.integration.apprise_activate": "将新文章推送到 Apprise",
|
||||||
"form.integration.apprise_url": "Apprise API URL",
|
"form.integration.apprise_url": "Apprise API URL",
|
||||||
"form.integration.apprise_services_url": "使用逗号分隔的 Apprise 服务 URL 列表",
|
"form.integration.apprise_services_url": "使用逗号分隔的 Apprise 服务 URL 列表",
|
||||||
@ -435,13 +406,13 @@
|
|||||||
"form.integration.omnivore_activate": "保存文章到 Omnivore",
|
"form.integration.omnivore_activate": "保存文章到 Omnivore",
|
||||||
"form.integration.omnivore_url": "Omnivore API 端点",
|
"form.integration.omnivore_url": "Omnivore API 端点",
|
||||||
"form.integration.omnivore_api_key": "Omnivore API 密钥",
|
"form.integration.omnivore_api_key": "Omnivore API 密钥",
|
||||||
"form.integration.espial_activate": "保存文章到 Espial",
|
"form.integration.espial_activate": "保存文章到 Espial",
|
||||||
"form.integration.espial_endpoint": "Espial API 端点",
|
"form.integration.espial_endpoint": "Espial API 端点",
|
||||||
"form.integration.espial_api_key": "Espial API 密钥",
|
"form.integration.espial_api_key": "Espial API 密钥",
|
||||||
"form.integration.espial_tags": "Espial 标签",
|
"form.integration.espial_tags": "Espial 标签",
|
||||||
"form.integration.readwise_activate": "保存文章到 Readwise Reader",
|
"form.integration.readwise_activate": "保存文章到 Readwise Reader",
|
||||||
"form.integration.readwise_api_key": "Readwise Reader 密钥",
|
"form.integration.readwise_api_key": "Readwise Reader Access Token",
|
||||||
"form.integration.readwise_api_key_link": "获取你的 Readwise 密钥",
|
"form.integration.readwise_api_key_link": "获取你的 Readwise Access Token",
|
||||||
"form.integration.telegram_bot_activate": "将新文章推送到 Telegram",
|
"form.integration.telegram_bot_activate": "将新文章推送到 Telegram",
|
||||||
"form.integration.telegram_bot_token": "机器人令牌",
|
"form.integration.telegram_bot_token": "机器人令牌",
|
||||||
"form.integration.telegram_topic_id": "Topic ID",
|
"form.integration.telegram_topic_id": "Topic ID",
|
||||||
@ -449,12 +420,12 @@
|
|||||||
"form.integration.telegram_bot_disable_notification": "禁用通知",
|
"form.integration.telegram_bot_disable_notification": "禁用通知",
|
||||||
"form.integration.telegram_bot_disable_buttons": "不展示按钮",
|
"form.integration.telegram_bot_disable_buttons": "不展示按钮",
|
||||||
"form.integration.telegram_chat_id": "聊天ID",
|
"form.integration.telegram_chat_id": "聊天ID",
|
||||||
"form.integration.linkace_activate": "保存文章到 LinkAce",
|
"form.integration.linkace_activate": "Save entries to LinkAce",
|
||||||
"form.integration.linkace_endpoint": "LinkAce API URL",
|
"form.integration.linkace_endpoint": "LinkAce API Endpoint",
|
||||||
"form.integration.linkace_api_key": "LinkAce API 密钥",
|
"form.integration.linkace_api_key": "LinkAce API key",
|
||||||
"form.integration.linkace_tags": "LinkAce 标签",
|
"form.integration.linkace_tags": "LinkAce Tags",
|
||||||
"form.integration.linkace_is_private": "将链接标记为私有",
|
"form.integration.linkace_is_private": "Mark link as private",
|
||||||
"form.integration.linkace_check_disabled": "关闭链接检查",
|
"form.integration.linkace_check_disabled": "Disable link check",
|
||||||
"form.integration.linkding_activate": "保存文章到 Linkding",
|
"form.integration.linkding_activate": "保存文章到 Linkding",
|
||||||
"form.integration.linkding_endpoint": "Linkding API 端点",
|
"form.integration.linkding_endpoint": "Linkding API 端点",
|
||||||
"form.integration.linkding_api_key": "Linkding API 密钥",
|
"form.integration.linkding_api_key": "Linkding API 密钥",
|
||||||
@ -468,10 +439,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Matrix Bot 密码",
|
"form.integration.matrix_bot_password": "Matrix Bot 密码",
|
||||||
"form.integration.matrix_bot_url": "Matrix 服务器 URL",
|
"form.integration.matrix_bot_url": "Matrix 服务器 URL",
|
||||||
"form.integration.matrix_bot_chat_id": "Matrix 聊天 ID",
|
"form.integration.matrix_bot_chat_id": "Matrix 聊天 ID",
|
||||||
"form.integration.raindrop_activate": "保存文章到 Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) 密钥",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "保存文章到 Readeck",
|
"form.integration.readeck_activate": "保存文章到 Readeck",
|
||||||
"form.integration.readeck_endpoint": "Readeck API 端点",
|
"form.integration.readeck_endpoint": "Readeck API 端点",
|
||||||
"form.integration.readeck_api_key": "Readeck API 密钥",
|
"form.integration.readeck_api_key": "Readeck API 密钥",
|
||||||
@ -483,19 +450,12 @@
|
|||||||
"form.integration.shiori_password": "Shiori 密码",
|
"form.integration.shiori_password": "Shiori 密码",
|
||||||
"form.integration.shaarli_activate": "保存文章到 Shaarli",
|
"form.integration.shaarli_activate": "保存文章到 Shaarli",
|
||||||
"form.integration.shaarli_endpoint": "Shaarli URL",
|
"form.integration.shaarli_endpoint": "Shaarli URL",
|
||||||
"form.integration.shaarli_api_secret": "Shaarli API 密钥",
|
"form.integration.shaarli_api_secret": "Shaarli API Secret",
|
||||||
"form.integration.webhook_activate": "启用 Webhook",
|
"form.integration.webhook_activate": "启用 Webhook",
|
||||||
"form.integration.webhook_url": "Webhook URL",
|
"form.integration.webhook_url": "Webhook URL",
|
||||||
"form.integration.webhook_secret": "Webhook 密钥",
|
"form.integration.webhook_secret": "Webhook Secret",
|
||||||
"form.integration.rssbridge_activate": "添加订阅时检查 RSS-Bridge",
|
"form.integration.rssbridge_activate": "添加订阅时检查 RSS-Bridge",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge 服务器 URL",
|
"form.integration.rssbridge_url": "RSS-Bridge 服务器 URL",
|
||||||
"form.integration.ntfy_activate": "推送条目到ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy主题",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL(可选,默认为ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API令牌(可选)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy用户名(可选)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy密码(可选)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy图标URL(可选)",
|
|
||||||
"form.api_key.label.description": "API密钥标签",
|
"form.api_key.label.description": "API密钥标签",
|
||||||
"form.submit.loading": "载入中…",
|
"form.submit.loading": "载入中…",
|
||||||
"form.submit.saving": "保存中…",
|
"form.submit.saving": "保存中…",
|
||||||
@ -521,42 +481,31 @@
|
|||||||
"%d 年前"
|
"%d 年前"
|
||||||
],
|
],
|
||||||
"alert.too_many_feeds_refresh": [
|
"alert.too_many_feeds_refresh": [
|
||||||
"多次触发订阅源更新,请等待 %d 分钟后重试。"
|
"You have triggered too many feed refreshes. Please wait %d minute before trying again."
|
||||||
],
|
],
|
||||||
"alert.background_feed_refresh": "所有的订阅源都在后台刷新中。您可以继续使用Miniflux,同时此过程正在运行。",
|
"alert.background_feed_refresh": "All feeds are being refreshed in the background. You can continue to use Miniflux while this process is running.",
|
||||||
"error.http_response_too_large": "HTTP响应内容过大,您可以在全局设置中增加HTTP响应大小限制(需要服务器重新启动)。",
|
"error.http_response_too_large": "The HTTP response is too large. You could increase the HTTP response size limit in the global settings (requires a server restart).",
|
||||||
"error.http_body_read": "无法读取HTTP主体: %v。",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "HTTP响应主体为空。",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "HTTP响应内容为空,该网站可能正在使用机器人保护机制。",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS 错误: %q。如果您愿意的话可以在订阅源设置里关闭TLS验证。",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux无法访问该网站由于网络错误: %v。",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "该网站响应过慢,请求超时: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP 客户端错误r: %v。",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
"error.http_not_authorized": "该网站访问未授权,可能用户名和密码错误。",
|
"error.http_not_authorized": "Access to this website is not authorized. It could be a bad username or password.",
|
||||||
"error.http_too_many_requests": "Miniflux 对该网站请求过多次数,请稍后重试或修改应用配置项。",
|
"error.http_too_many_requests": "Miniflux generated too many requests to this website. Please, try again later or change the application configuration.",
|
||||||
"error.http_forbidden": "该网站被禁止访问,网站可能有机器人保护机制?",
|
"error.http_forbidden": "Access to this website is forbidden. Perhaps, this website has a bot protection mechanism?",
|
||||||
"error.http_resource_not_found": "请求资源无法找到,请检查URL。",
|
"error.http_resource_not_found": "The requested resource is not found. Please, verify the URL.",
|
||||||
"error.http_internal_server_error": "当前由于服务器错误导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
"error.http_internal_server_error": "The website is not available at the moment due to a server error. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.http_bad_gateway": "当前由于错误的网关导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
"error.http_bad_gateway": "The website is not available at the moment due to a bad gateway error. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.http_service_unavailable": "当前由于服务器内部错误导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
"error.http_service_unavailable": "The website is not available at the moment due to an internal server error. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.http_gateway_timeout": "当前由于网关超时导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
"error.http_gateway_timeout": "The website is not available at the moment due to a gateway timeout error. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.http_unexpected_status_code": "当前由于意外的HTTP状态码:%d 导致该网站无法访问,问题不在Miniflux,请稍后重试。",
|
"error.http_unexpected_status_code": "The website is not available at the moment due to an unexpected HTTP status code: %d. The problem is not on Miniflux side. Please, try again later.",
|
||||||
"error.database_error": "数据库错误: %v。",
|
"error.database_error": "Database error: %v.",
|
||||||
"error.category_not_found": "该分类不存在或不属于该用户。",
|
"error.category_not_found": "This category does not exist or does not belong to this user.",
|
||||||
"error.duplicated_feed": "该订阅源已经存在。",
|
"error.duplicated_feed": "This feed already exists.",
|
||||||
"error.unable_to_parse_feed": "无法解析该订阅源: %v。",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "该订阅源不存在或不属于该用户。",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "无法使用RSS-Bridge去检测订阅源: %v。",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "无法解析订阅源格式: %v。",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "音频/视频的播放速度",
|
|
||||||
"error.settings_media_playback_rate_range": "播放速度超出范围",
|
|
||||||
"enclosure_media_controls.seek" : "查找:",
|
|
||||||
"enclosure_media_controls.seek.title" : "查找 %s 秒",
|
|
||||||
"enclosure_media_controls.speed" : "速度:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "快进",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "速度快进到 %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "减慢",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "速度减慢到 %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "重置",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "重置速度到 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@
|
|||||||
"menu.mark_all_as_read": "全部標為已讀",
|
"menu.mark_all_as_read": "全部標為已讀",
|
||||||
"menu.show_all_entries": "顯示所有文章",
|
"menu.show_all_entries": "顯示所有文章",
|
||||||
"menu.show_only_unread_entries": "僅顯示未讀文章",
|
"menu.show_only_unread_entries": "僅顯示未讀文章",
|
||||||
"menu.show_only_starred_entries": "Show only starred entries",
|
|
||||||
"menu.refresh_feed": "更新",
|
"menu.refresh_feed": "更新",
|
||||||
"menu.refresh_all_feeds": "背景更新全部Feeds",
|
"menu.refresh_all_feeds": "背景更新全部Feeds",
|
||||||
"menu.edit_feed": "編輯",
|
"menu.edit_feed": "編輯",
|
||||||
@ -56,9 +55,7 @@
|
|||||||
"search.label": "搜尋",
|
"search.label": "搜尋",
|
||||||
"search.placeholder": "搜尋…",
|
"search.placeholder": "搜尋…",
|
||||||
"search.submit": "送出",
|
"search.submit": "送出",
|
||||||
"pagination.last": "Last",
|
|
||||||
"pagination.next": "下一頁",
|
"pagination.next": "下一頁",
|
||||||
"pagination.first": "First",
|
|
||||||
"pagination.previous": "上一頁",
|
"pagination.previous": "上一頁",
|
||||||
"entry.status.unread": "標為未讀",
|
"entry.status.unread": "標為未讀",
|
||||||
"entry.status.read": "標為已讀",
|
"entry.status.read": "標為已讀",
|
||||||
@ -172,8 +169,6 @@
|
|||||||
"page.keyboard_shortcuts.go_to_feed": "轉到Feed頁面",
|
"page.keyboard_shortcuts.go_to_feed": "轉到Feed頁面",
|
||||||
"page.keyboard_shortcuts.go_to_previous_page": "上一頁",
|
"page.keyboard_shortcuts.go_to_previous_page": "上一頁",
|
||||||
"page.keyboard_shortcuts.go_to_next_page": "下一頁",
|
"page.keyboard_shortcuts.go_to_next_page": "下一頁",
|
||||||
"page.keyboard_shortcuts.go_to_bottom_item": "转到底部项目",
|
|
||||||
"page.keyboard_shortcuts.go_to_top_item": "转到顶部项目",
|
|
||||||
"page.keyboard_shortcuts.open_item": "開啟選定的文章",
|
"page.keyboard_shortcuts.open_item": "開啟選定的文章",
|
||||||
"page.keyboard_shortcuts.open_original": "開啟原始連結",
|
"page.keyboard_shortcuts.open_original": "開啟原始連結",
|
||||||
"page.keyboard_shortcuts.open_original_same_window": "在當前標籤頁中開啟原始連結",
|
"page.keyboard_shortcuts.open_original_same_window": "在當前標籤頁中開啟原始連結",
|
||||||
@ -202,8 +197,8 @@
|
|||||||
"page.settings.title": "設定",
|
"page.settings.title": "設定",
|
||||||
"page.settings.link_google_account": "關聯我的 Google 賬戶",
|
"page.settings.link_google_account": "關聯我的 Google 賬戶",
|
||||||
"page.settings.unlink_google_account": "解除 Google 帳號關聯",
|
"page.settings.unlink_google_account": "解除 Google 帳號關聯",
|
||||||
"page.settings.link_oidc_account": "關聯我的 %s 賬戶",
|
"page.settings.link_oidc_account": "關聯我的 OpenID Connect 賬戶",
|
||||||
"page.settings.unlink_oidc_account": "解除 %s 帳號關聯",
|
"page.settings.unlink_oidc_account": "解除 OpenID Connect 帳號關聯",
|
||||||
"page.settings.webauthn.passkeys": "Passkeys",
|
"page.settings.webauthn.passkeys": "Passkeys",
|
||||||
"page.settings.webauthn.actions": "操作",
|
"page.settings.webauthn.actions": "操作",
|
||||||
"page.settings.webauthn.passkey_name": "Passkey 名稱",
|
"page.settings.webauthn.passkey_name": "Passkey 名稱",
|
||||||
@ -216,7 +211,7 @@
|
|||||||
],
|
],
|
||||||
"page.login.title": "登入",
|
"page.login.title": "登入",
|
||||||
"page.login.google_signin": "使用 Google 登入",
|
"page.login.google_signin": "使用 Google 登入",
|
||||||
"page.login.oidc_signin": "使用 %s 登入",
|
"page.login.oidc_signin": "使用 OpenID Connect 登入",
|
||||||
"page.login.webauthn_login": "使用密碼登錄",
|
"page.login.webauthn_login": "使用密碼登錄",
|
||||||
"page.login.webauthn_login.error": "無法使用密碼登錄",
|
"page.login.webauthn_login.error": "無法使用密碼登錄",
|
||||||
"page.integrations.title": "整合",
|
"page.integrations.title": "整合",
|
||||||
@ -251,7 +246,6 @@
|
|||||||
"alert.no_bookmark": "目前沒有收藏",
|
"alert.no_bookmark": "目前沒有收藏",
|
||||||
"alert.no_category": "目前沒有分類",
|
"alert.no_category": "目前沒有分類",
|
||||||
"alert.no_category_entry": "該分類下沒有文章",
|
"alert.no_category_entry": "該分類下沒有文章",
|
||||||
"alert.no_tag_entry": "沒有與此標籤相符的條目。",
|
|
||||||
"alert.no_feed_entry": "該Feed中沒有文章",
|
"alert.no_feed_entry": "該Feed中沒有文章",
|
||||||
"alert.no_feed": "目前沒有Feed",
|
"alert.no_feed": "目前沒有Feed",
|
||||||
"alert.no_history": "目前沒有歷史",
|
"alert.no_history": "目前沒有歷史",
|
||||||
@ -286,14 +280,6 @@
|
|||||||
"error.password_min_length": "請至少輸入 6 個字元",
|
"error.password_min_length": "請至少輸入 6 個字元",
|
||||||
"error.settings_mandatory_fields": "必須填寫使用者名稱、主題、語言以及時區",
|
"error.settings_mandatory_fields": "必須填寫使用者名稱、主題、語言以及時區",
|
||||||
"error.settings_reading_speed_is_positive": "閱讀速度必須是正整數。",
|
"error.settings_reading_speed_is_positive": "閱讀速度必須是正整數。",
|
||||||
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_block_rule_separator_required": "Invalid Block rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
|
|
||||||
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.settings_keep_rule_fieldname_invalid": "Invalid Keep rule: rule #%d is missing a valid field name (Options: %s)",
|
|
||||||
"error.settings_keep_rule_separator_required": "Invalid Keep rule: rule #%d's pattern is required to be seperated by a '='",
|
|
||||||
"error.settings_keep_rule_regex_required": "Invalid Keep rule: rule #%d pattern is not provided",
|
|
||||||
"error.settings_keep_rule_invalid_regex": "Invalid Keep rule: rule #%d's pattern is not a valid regex",
|
|
||||||
"error.entries_per_page_invalid": "每頁的文章數無效。",
|
"error.entries_per_page_invalid": "每頁的文章數無效。",
|
||||||
"error.feed_mandatory_fields": "必須填寫網址和分類",
|
"error.feed_mandatory_fields": "必須填寫網址和分類",
|
||||||
"error.feed_already_exists": "此Feed已存在。",
|
"error.feed_already_exists": "此Feed已存在。",
|
||||||
@ -318,7 +304,6 @@
|
|||||||
"form.feed.label.title": "標題",
|
"form.feed.label.title": "標題",
|
||||||
"form.feed.label.site_url": "網站 URL",
|
"form.feed.label.site_url": "網站 URL",
|
||||||
"form.feed.label.feed_url": "訂閱 Feed URL",
|
"form.feed.label.feed_url": "訂閱 Feed URL",
|
||||||
"form.feed.label.description": "描述",
|
|
||||||
"form.feed.label.category": "類別",
|
"form.feed.label.category": "類別",
|
||||||
"form.feed.label.crawler": "下載原文內容",
|
"form.feed.label.crawler": "下載原文內容",
|
||||||
"form.feed.label.feed_username": "Feed 使用者名稱",
|
"form.feed.label.feed_username": "Feed 使用者名稱",
|
||||||
@ -338,13 +323,6 @@
|
|||||||
"form.feed.label.disabled": "請勿更新此 Feed",
|
"form.feed.label.disabled": "請勿更新此 Feed",
|
||||||
"form.feed.label.no_media_player": "沒有媒體播放器(音訊/視訊)",
|
"form.feed.label.no_media_player": "沒有媒體播放器(音訊/視訊)",
|
||||||
"form.feed.label.hide_globally": "隱藏全域性未讀列表中的文章",
|
"form.feed.label.hide_globally": "隱藏全域性未讀列表中的文章",
|
||||||
"form.feed.label.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.feed.label.ntfy_priority": "Ntfy priority",
|
|
||||||
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
|
|
||||||
"form.feed.label.ntfy_high_priority": "Ntfy high priority",
|
|
||||||
"form.feed.label.ntfy_default_priority": "Ntfy default priority",
|
|
||||||
"form.feed.label.ntfy_low_priority": "Ntfy low priority",
|
|
||||||
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
|
|
||||||
"form.feed.fieldset.general": "通用",
|
"form.feed.fieldset.general": "通用",
|
||||||
"form.feed.fieldset.rules": "規則",
|
"form.feed.fieldset.rules": "規則",
|
||||||
"form.feed.fieldset.network_settings": "網路設定",
|
"form.feed.fieldset.network_settings": "網路設定",
|
||||||
@ -385,18 +363,11 @@
|
|||||||
"form.prefs.label.default_home_page": "預設主頁",
|
"form.prefs.label.default_home_page": "預設主頁",
|
||||||
"form.prefs.label.categories_sorting_order": "分類排序",
|
"form.prefs.label.categories_sorting_order": "分類排序",
|
||||||
"form.prefs.label.mark_read_on_view": "查看時自動將條目標記為已讀",
|
"form.prefs.label.mark_read_on_view": "查看時自動將條目標記為已讀",
|
||||||
"form.prefs.label.mark_read_on_view_or_media_completion": "Mark entries as read when viewed. For audio/video, mark as read at 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_on_media_completion": "Only mark as read when audio/video playback reaches 90%% completion",
|
|
||||||
"form.prefs.label.mark_read_manually": "Mark entries as read manually",
|
|
||||||
"form.prefs.fieldset.application_settings": "應用程式設定",
|
"form.prefs.fieldset.application_settings": "應用程式設定",
|
||||||
"form.prefs.fieldset.authentication_settings": "使用者認證設定",
|
"form.prefs.fieldset.authentication_settings": "使用者認證設定",
|
||||||
"form.prefs.fieldset.reader_settings": "閱讀器設定",
|
"form.prefs.fieldset.reader_settings": "閱讀器設定",
|
||||||
"form.prefs.fieldset.global_feed_settings": "Global Feed Settings",
|
|
||||||
"form.import.label.file": "OPML 檔案",
|
"form.import.label.file": "OPML 檔案",
|
||||||
"form.import.label.url": "URL",
|
"form.import.label.url": "URL",
|
||||||
"form.integration.betula_activate": "Save entries to Betula",
|
|
||||||
"form.integration.betula_url": "Betula server URL",
|
|
||||||
"form.integration.betula_token": "Betula Token",
|
|
||||||
"form.integration.fever_activate": "啟用 Fever API",
|
"form.integration.fever_activate": "啟用 Fever API",
|
||||||
"form.integration.fever_username": "Fever 使用者名稱",
|
"form.integration.fever_username": "Fever 使用者名稱",
|
||||||
"form.integration.fever_password": "Fever 密碼",
|
"form.integration.fever_password": "Fever 密碼",
|
||||||
@ -468,10 +439,6 @@
|
|||||||
"form.integration.matrix_bot_password": "Matrix 的密碼",
|
"form.integration.matrix_bot_password": "Matrix 的密碼",
|
||||||
"form.integration.matrix_bot_url": "Matrix 伺服器的 URL",
|
"form.integration.matrix_bot_url": "Matrix 伺服器的 URL",
|
||||||
"form.integration.matrix_bot_chat_id": "Matrix 房間 ID",
|
"form.integration.matrix_bot_chat_id": "Matrix 房間 ID",
|
||||||
"form.integration.raindrop_activate": "Save entries to Raindrop",
|
|
||||||
"form.integration.raindrop_token": "(Test) Token",
|
|
||||||
"form.integration.raindrop_collection_id": "Collection ID",
|
|
||||||
"form.integration.raindrop_tags": "Tags (comma-separated)",
|
|
||||||
"form.integration.readeck_activate": "儲存文章到 Readeck",
|
"form.integration.readeck_activate": "儲存文章到 Readeck",
|
||||||
"form.integration.readeck_endpoint": "Readeck API 端點",
|
"form.integration.readeck_endpoint": "Readeck API 端點",
|
||||||
"form.integration.readeck_api_key": "Readeck API 金鑰",
|
"form.integration.readeck_api_key": "Readeck API 金鑰",
|
||||||
@ -489,13 +456,6 @@
|
|||||||
"form.integration.webhook_secret": "Webhook Secret",
|
"form.integration.webhook_secret": "Webhook Secret",
|
||||||
"form.integration.rssbridge_activate": "新增訂閱時檢查 RSS-Bridge",
|
"form.integration.rssbridge_activate": "新增訂閱時檢查 RSS-Bridge",
|
||||||
"form.integration.rssbridge_url": "RSS-Bridge 伺服器的 URL",
|
"form.integration.rssbridge_url": "RSS-Bridge 伺服器的 URL",
|
||||||
"form.integration.ntfy_activate": "Push entries to ntfy",
|
|
||||||
"form.integration.ntfy_topic": "Ntfy topic",
|
|
||||||
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
|
|
||||||
"form.integration.ntfy_api_token": "Ntfy API Token (optional)",
|
|
||||||
"form.integration.ntfy_username": "Ntfy Username (optional)",
|
|
||||||
"form.integration.ntfy_password": "Ntfy Password (optional)",
|
|
||||||
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
|
|
||||||
"form.api_key.label.description": "API金鑰標籤",
|
"form.api_key.label.description": "API金鑰標籤",
|
||||||
"form.submit.loading": "載入中…",
|
"form.submit.loading": "載入中…",
|
||||||
"form.submit.saving": "儲存中…",
|
"form.submit.saving": "儲存中…",
|
||||||
@ -528,7 +488,7 @@
|
|||||||
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
"error.http_body_read": "Unable to read the HTTP body: %v.",
|
||||||
"error.http_empty_response_body": "The HTTP response body is empty.",
|
"error.http_empty_response_body": "The HTTP response body is empty.",
|
||||||
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
"error.http_empty_response": "The HTTP response is empty. Perhaps, this website is using a bot protection mechanism?",
|
||||||
"error.tls_error": "TLS error: %q. You could disable TLS verification in the feed settings if you would like.",
|
"error.tls_error": "TLS error: %v. You could disable TLS verification in the feed settings if you would like.",
|
||||||
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
"error.network_operation": "Miniflux is not able to reach this website due to a network error: %v.",
|
||||||
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
"error.network_timeout": "This website is too slow and the request timed out: %v",
|
||||||
"error.http_client_error": "HTTP client error: %v.",
|
"error.http_client_error": "HTTP client error: %v.",
|
||||||
@ -547,16 +507,5 @@
|
|||||||
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
"error.unable_to_parse_feed": "Unable to parse this feed: %v.",
|
||||||
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
"error.feed_not_found": "This feed does not exist or does not belong to this user.",
|
||||||
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
"error.unable_to_detect_rssbridge": "Unable to detect feed using RSS-Bridge: %v.",
|
||||||
"error.feed_format_not_detected": "Unable to detect feed format: %v.",
|
"error.feed_format_not_detected": "Unable to detect feed format: %v."
|
||||||
"form.prefs.label.media_playback_rate": "音訊/視訊的播放速度",
|
|
||||||
"error.settings_media_playback_rate_range": "播放速度超出範圍",
|
|
||||||
"enclosure_media_controls.seek" : "Seek:",
|
|
||||||
"enclosure_media_controls.seek.title" : "Seek %s seconds",
|
|
||||||
"enclosure_media_controls.speed" : "Speed:",
|
|
||||||
"enclosure_media_controls.speed.faster" : "Faster",
|
|
||||||
"enclosure_media_controls.speed.faster.title" : "Faster by %sx",
|
|
||||||
"enclosure_media_controls.speed.slower" : "Slower",
|
|
||||||
"enclosure_media_controls.speed.slower.title" : "Slower by %sx",
|
|
||||||
"enclosure_media_controls.speed.reset" : "Reset",
|
|
||||||
"enclosure_media_controls.speed.reset.title" : "Reset speed to 1x"
|
|
||||||
}
|
}
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package mediaproxy // import "miniflux.app/v2/internal/mediaproxy"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"log/slog"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
|
|
||||||
"miniflux.app/v2/internal/config"
|
|
||||||
"miniflux.app/v2/internal/http/route"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ProxifyRelativeURL(router *mux.Router, mediaURL string) string {
|
|
||||||
if mediaURL == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if customProxyURL := config.Opts.MediaCustomProxyURL(); customProxyURL != "" {
|
|
||||||
return proxifyURLWithCustomProxy(mediaURL, customProxyURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
mac := hmac.New(sha256.New, config.Opts.MediaProxyPrivateKey())
|
|
||||||
mac.Write([]byte(mediaURL))
|
|
||||||
digest := mac.Sum(nil)
|
|
||||||
return route.Path(router, "proxy", "encodedDigest", base64.URLEncoding.EncodeToString(digest), "encodedURL", base64.URLEncoding.EncodeToString([]byte(mediaURL)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ProxifyAbsoluteURL(router *mux.Router, mediaURL string) string {
|
|
||||||
if mediaURL == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if customProxyURL := config.Opts.MediaCustomProxyURL(); customProxyURL != "" {
|
|
||||||
return proxifyURLWithCustomProxy(mediaURL, customProxyURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note that the proxyified URL is relative to the root URL.
|
|
||||||
proxifiedUrl := ProxifyRelativeURL(router, mediaURL)
|
|
||||||
absoluteURL, err := url.JoinPath(config.Opts.RootURL(), proxifiedUrl)
|
|
||||||
if err != nil {
|
|
||||||
return mediaURL
|
|
||||||
}
|
|
||||||
|
|
||||||
return absoluteURL
|
|
||||||
}
|
|
||||||
|
|
||||||
func proxifyURLWithCustomProxy(mediaURL, customProxyURL string) string {
|
|
||||||
if customProxyURL == "" {
|
|
||||||
return mediaURL
|
|
||||||
}
|
|
||||||
|
|
||||||
absoluteURL, err := url.JoinPath(customProxyURL, base64.URLEncoding.EncodeToString([]byte(mediaURL)))
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Incorrect custom media proxy URL",
|
|
||||||
slog.String("custom_proxy_url", customProxyURL),
|
|
||||||
slog.Any("error", err),
|
|
||||||
)
|
|
||||||
return mediaURL
|
|
||||||
}
|
|
||||||
|
|
||||||
return absoluteURL
|
|
||||||
}
|
|
@ -2,14 +2,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package model // import "miniflux.app/v2/internal/model"
|
package model // import "miniflux.app/v2/internal/model"
|
||||||
import (
|
import "strings"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"miniflux.app/v2/internal/config"
|
|
||||||
"miniflux.app/v2/internal/mediaproxy"
|
|
||||||
"miniflux.app/v2/internal/urllib"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Enclosure represents an attachment.
|
// Enclosure represents an attachment.
|
||||||
type Enclosure struct {
|
type Enclosure struct {
|
||||||
@ -22,56 +15,20 @@ type Enclosure struct {
|
|||||||
MediaProgression int64 `json:"media_progression"`
|
MediaProgression int64 `json:"media_progression"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnclosureUpdateRequest struct {
|
|
||||||
MediaProgression int64 `json:"media_progression"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Html5MimeType will modify the actual MimeType to allow direct playback from HTML5 player for some kind of MimeType
|
// Html5MimeType will modify the actual MimeType to allow direct playback from HTML5 player for some kind of MimeType
|
||||||
func (e Enclosure) Html5MimeType() string {
|
func (e Enclosure) Html5MimeType() string {
|
||||||
if e.MimeType == "video/m4v" {
|
if strings.HasPrefix(e.MimeType, "video") {
|
||||||
return "video/x-m4v"
|
switch e.MimeType {
|
||||||
|
// Solution from this stackoverflow discussion:
|
||||||
|
// https://stackoverflow.com/questions/15277147/m4v-mimetype-video-mp4-or-video-m4v/66945470#66945470
|
||||||
|
// tested at the time of this commit (06/2023) on latest Firefox & Vivaldi on this feed
|
||||||
|
// https://www.florenceporcel.com/podcast/lfhdu.xml
|
||||||
|
case "video/m4v":
|
||||||
|
return "video/x-m4v"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return e.MimeType
|
return e.MimeType
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnclosureList represents a list of attachments.
|
// EnclosureList represents a list of attachments.
|
||||||
type EnclosureList []*Enclosure
|
type EnclosureList []*Enclosure
|
||||||
|
|
||||||
func (el EnclosureList) ContainsAudioOrVideo() bool {
|
|
||||||
for _, enclosure := range el {
|
|
||||||
if strings.Contains(enclosure.MimeType, "audio/") || strings.Contains(enclosure.MimeType, "video/") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (el EnclosureList) ProxifyEnclosureURL(router *mux.Router) {
|
|
||||||
proxyOption := config.Opts.MediaProxyMode()
|
|
||||||
|
|
||||||
if proxyOption == "all" || proxyOption != "none" {
|
|
||||||
for i := range el {
|
|
||||||
if urllib.IsHTTPS(el[i].URL) {
|
|
||||||
for _, mediaType := range config.Opts.MediaProxyResourceTypes() {
|
|
||||||
if strings.HasPrefix(el[i].MimeType, mediaType+"/") {
|
|
||||||
el[i].URL = mediaproxy.ProxifyAbsoluteURL(router, el[i].URL)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Enclosure) ProxifyEnclosureURL(router *mux.Router) {
|
|
||||||
proxyOption := config.Opts.MediaProxyMode()
|
|
||||||
|
|
||||||
if proxyOption == "all" || proxyOption != "none" && !urllib.IsHTTPS(e.URL) {
|
|
||||||
for _, mediaType := range config.Opts.MediaProxyResourceTypes() {
|
|
||||||
if strings.HasPrefix(e.MimeType, mediaType+"/") {
|
|
||||||
e.URL = mediaproxy.ProxifyAbsoluteURL(router, e.URL)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -50,22 +50,6 @@ func NewEntry() *Entry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldMarkAsReadOnView Return whether the entry should be marked as viewed considering all user settings and entry state.
|
|
||||||
func (e *Entry) ShouldMarkAsReadOnView(user *User) bool {
|
|
||||||
// Already read, no need to mark as read again. Removed entries are not marked as read
|
|
||||||
if e.Status != EntryStatusUnread {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// There is an enclosure, markAsRead will happen at enclosure completion time, no need to mark as read on view
|
|
||||||
if user.MarkReadOnMediaPlayerCompletion && e.Enclosures.ContainsAudioOrVideo() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// The user wants to mark as read on view
|
|
||||||
return user.MarkReadOnView
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entries represents a list of entries.
|
// Entries represents a list of entries.
|
||||||
type Entries []*Entry
|
type Entries []*Entry
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ type Feed struct {
|
|||||||
FeedURL string `json:"feed_url"`
|
FeedURL string `json:"feed_url"`
|
||||||
SiteURL string `json:"site_url"`
|
SiteURL string `json:"site_url"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
|
||||||
CheckedAt time.Time `json:"checked_at"`
|
CheckedAt time.Time `json:"checked_at"`
|
||||||
NextCheckAt time.Time `json:"next_check_at"`
|
NextCheckAt time.Time `json:"next_check_at"`
|
||||||
EtagHeader string `json:"etag_header"`
|
EtagHeader string `json:"etag_header"`
|
||||||
@ -51,10 +50,8 @@ type Feed struct {
|
|||||||
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
||||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||||
HideGlobally bool `json:"hide_globally"`
|
HideGlobally bool `json:"hide_globally"`
|
||||||
DisableHTTP2 bool `json:"disable_http2"`
|
|
||||||
AppriseServiceURLs string `json:"apprise_service_urls"`
|
AppriseServiceURLs string `json:"apprise_service_urls"`
|
||||||
NtfyEnabled bool `json:"ntfy_enabled"`
|
DisableHTTP2 bool `json:"disable_http2"`
|
||||||
NtfyPriority int `json:"ntfy_priority"`
|
|
||||||
|
|
||||||
// Non persisted attributes
|
// Non persisted attributes
|
||||||
Category *Category `json:"category,omitempty"`
|
Category *Category `json:"category,omitempty"`
|
||||||
@ -162,7 +159,25 @@ type FeedCreationRequestFromSubscriptionDiscovery struct {
|
|||||||
ETag string
|
ETag string
|
||||||
LastModified string
|
LastModified string
|
||||||
|
|
||||||
FeedCreationRequest
|
FeedURL string `json:"feed_url"`
|
||||||
|
CategoryID int64 `json:"category_id"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
Cookie string `json:"cookie"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Crawler bool `json:"crawler"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
NoMediaPlayer bool `json:"no_media_player"`
|
||||||
|
IgnoreHTTPCache bool `json:"ignore_http_cache"`
|
||||||
|
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
||||||
|
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||||
|
ScraperRules string `json:"scraper_rules"`
|
||||||
|
RewriteRules string `json:"rewrite_rules"`
|
||||||
|
BlocklistRules string `json:"blocklist_rules"`
|
||||||
|
KeeplistRules string `json:"keeplist_rules"`
|
||||||
|
HideGlobally bool `json:"hide_globally"`
|
||||||
|
UrlRewriteRules string `json:"urlrewrite_rules"`
|
||||||
|
DisableHTTP2 bool `json:"disable_http2"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FeedModificationRequest represents the request to update a feed.
|
// FeedModificationRequest represents the request to update a feed.
|
||||||
@ -170,7 +185,6 @@ type FeedModificationRequest struct {
|
|||||||
FeedURL *string `json:"feed_url"`
|
FeedURL *string `json:"feed_url"`
|
||||||
SiteURL *string `json:"site_url"`
|
SiteURL *string `json:"site_url"`
|
||||||
Title *string `json:"title"`
|
Title *string `json:"title"`
|
||||||
Description *string `json:"description"`
|
|
||||||
ScraperRules *string `json:"scraper_rules"`
|
ScraperRules *string `json:"scraper_rules"`
|
||||||
RewriteRules *string `json:"rewrite_rules"`
|
RewriteRules *string `json:"rewrite_rules"`
|
||||||
BlocklistRules *string `json:"blocklist_rules"`
|
BlocklistRules *string `json:"blocklist_rules"`
|
||||||
@ -205,10 +219,6 @@ func (f *FeedModificationRequest) Patch(feed *Feed) {
|
|||||||
feed.Title = *f.Title
|
feed.Title = *f.Title
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Description != nil && *f.Description != "" {
|
|
||||||
feed.Description = *f.Description
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.ScraperRules != nil {
|
if f.ScraperRules != nil {
|
||||||
feed.ScraperRules = *f.ScraperRules
|
feed.ScraperRules = *f.ScraperRules
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,6 @@ package model // import "miniflux.app/v2/internal/model"
|
|||||||
// Integration represents user integration settings.
|
// Integration represents user integration settings.
|
||||||
type Integration struct {
|
type Integration struct {
|
||||||
UserID int64
|
UserID int64
|
||||||
BetulaEnabled bool
|
|
||||||
BetulaURL string
|
|
||||||
BetulaToken string
|
|
||||||
PinboardEnabled bool
|
PinboardEnabled bool
|
||||||
PinboardToken string
|
PinboardToken string
|
||||||
PinboardTags string
|
PinboardTags string
|
||||||
@ -93,15 +90,4 @@ type Integration struct {
|
|||||||
OmnivoreEnabled bool
|
OmnivoreEnabled bool
|
||||||
OmnivoreAPIKey string
|
OmnivoreAPIKey string
|
||||||
OmnivoreURL string
|
OmnivoreURL string
|
||||||
RaindropEnabled bool
|
|
||||||
RaindropToken string
|
|
||||||
RaindropCollectionID string
|
|
||||||
RaindropTags string
|
|
||||||
NtfyEnabled bool
|
|
||||||
NtfyTopic string
|
|
||||||
NtfyURL string
|
|
||||||
NtfyAPIToken string
|
|
||||||
NtfyUsername string
|
|
||||||
NtfyPassword string
|
|
||||||
NtfyIconURL string
|
|
||||||
}
|
}
|
||||||
|
@ -3,20 +3,26 @@
|
|||||||
|
|
||||||
package model // import "miniflux.app/v2/internal/model"
|
package model // import "miniflux.app/v2/internal/model"
|
||||||
|
|
||||||
type Number interface {
|
// OptionalString populates an optional string field.
|
||||||
int | int64 | float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func OptionalNumber[T Number](value T) *T {
|
|
||||||
if value > 0 {
|
|
||||||
return &value
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func OptionalString(value string) *string {
|
func OptionalString(value string) *string {
|
||||||
if value != "" {
|
if value != "" {
|
||||||
return &value
|
return &value
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OptionalInt populates an optional int field.
|
||||||
|
func OptionalInt(value int) *int {
|
||||||
|
if value > 0 {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionalInt64 populates an optional int64 field.
|
||||||
|
func OptionalInt64(value int64) *int64 {
|
||||||
|
if value > 0 {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -11,34 +11,30 @@ import (
|
|||||||
|
|
||||||
// User represents a user in the system.
|
// User represents a user in the system.
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"-"`
|
Password string `json:"-"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
Theme string `json:"theme"`
|
Theme string `json:"theme"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone"`
|
||||||
EntryDirection string `json:"entry_sorting_direction"`
|
EntryDirection string `json:"entry_sorting_direction"`
|
||||||
EntryOrder string `json:"entry_sorting_order"`
|
EntryOrder string `json:"entry_sorting_order"`
|
||||||
Stylesheet string `json:"stylesheet"`
|
Stylesheet string `json:"stylesheet"`
|
||||||
GoogleID string `json:"google_id"`
|
GoogleID string `json:"google_id"`
|
||||||
OpenIDConnectID string `json:"openid_connect_id"`
|
OpenIDConnectID string `json:"openid_connect_id"`
|
||||||
EntriesPerPage int `json:"entries_per_page"`
|
EntriesPerPage int `json:"entries_per_page"`
|
||||||
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
||||||
ShowReadingTime bool `json:"show_reading_time"`
|
ShowReadingTime bool `json:"show_reading_time"`
|
||||||
EntrySwipe bool `json:"entry_swipe"`
|
EntrySwipe bool `json:"entry_swipe"`
|
||||||
GestureNav string `json:"gesture_nav"`
|
GestureNav string `json:"gesture_nav"`
|
||||||
LastLoginAt *time.Time `json:"last_login_at"`
|
LastLoginAt *time.Time `json:"last_login_at"`
|
||||||
DisplayMode string `json:"display_mode"`
|
DisplayMode string `json:"display_mode"`
|
||||||
DefaultReadingSpeed int `json:"default_reading_speed"`
|
DefaultReadingSpeed int `json:"default_reading_speed"`
|
||||||
CJKReadingSpeed int `json:"cjk_reading_speed"`
|
CJKReadingSpeed int `json:"cjk_reading_speed"`
|
||||||
DefaultHomePage string `json:"default_home_page"`
|
DefaultHomePage string `json:"default_home_page"`
|
||||||
CategoriesSortingOrder string `json:"categories_sorting_order"`
|
CategoriesSortingOrder string `json:"categories_sorting_order"`
|
||||||
MarkReadOnView bool `json:"mark_read_on_view"`
|
MarkReadOnView bool `json:"mark_read_on_view"`
|
||||||
MarkReadOnMediaPlayerCompletion bool `json:"mark_read_on_media_player_completion"`
|
|
||||||
MediaPlaybackRate float64 `json:"media_playback_rate"`
|
|
||||||
BlockFilterEntryRules string `json:"block_filter_entry_rules"`
|
|
||||||
KeepFilterEntryRules string `json:"keep_filter_entry_rules"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserCreationRequest represents the request to create a user.
|
// UserCreationRequest represents the request to create a user.
|
||||||
@ -52,32 +48,28 @@ type UserCreationRequest struct {
|
|||||||
|
|
||||||
// UserModificationRequest represents the request to update a user.
|
// UserModificationRequest represents the request to update a user.
|
||||||
type UserModificationRequest struct {
|
type UserModificationRequest struct {
|
||||||
Username *string `json:"username"`
|
Username *string `json:"username"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
Theme *string `json:"theme"`
|
Theme *string `json:"theme"`
|
||||||
Language *string `json:"language"`
|
Language *string `json:"language"`
|
||||||
Timezone *string `json:"timezone"`
|
Timezone *string `json:"timezone"`
|
||||||
EntryDirection *string `json:"entry_sorting_direction"`
|
EntryDirection *string `json:"entry_sorting_direction"`
|
||||||
EntryOrder *string `json:"entry_sorting_order"`
|
EntryOrder *string `json:"entry_sorting_order"`
|
||||||
Stylesheet *string `json:"stylesheet"`
|
Stylesheet *string `json:"stylesheet"`
|
||||||
GoogleID *string `json:"google_id"`
|
GoogleID *string `json:"google_id"`
|
||||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||||
EntriesPerPage *int `json:"entries_per_page"`
|
EntriesPerPage *int `json:"entries_per_page"`
|
||||||
IsAdmin *bool `json:"is_admin"`
|
IsAdmin *bool `json:"is_admin"`
|
||||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||||
ShowReadingTime *bool `json:"show_reading_time"`
|
ShowReadingTime *bool `json:"show_reading_time"`
|
||||||
EntrySwipe *bool `json:"entry_swipe"`
|
EntrySwipe *bool `json:"entry_swipe"`
|
||||||
GestureNav *string `json:"gesture_nav"`
|
GestureNav *string `json:"gesture_nav"`
|
||||||
DisplayMode *string `json:"display_mode"`
|
DisplayMode *string `json:"display_mode"`
|
||||||
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
||||||
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
||||||
DefaultHomePage *string `json:"default_home_page"`
|
DefaultHomePage *string `json:"default_home_page"`
|
||||||
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
||||||
MarkReadOnView *bool `json:"mark_read_on_view"`
|
MarkReadOnView *bool `json:"mark_read_on_view"`
|
||||||
MarkReadOnMediaPlayerCompletion *bool `json:"mark_read_on_media_player_completion"`
|
|
||||||
MediaPlaybackRate *float64 `json:"media_playback_rate"`
|
|
||||||
BlockFilterEntryRules *string `json:"block_filter_entry_rules"`
|
|
||||||
KeepFilterEntryRules *string `json:"keep_filter_entry_rules"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch updates the User object with the modification request.
|
// Patch updates the User object with the modification request.
|
||||||
@ -169,22 +161,6 @@ func (u *UserModificationRequest) Patch(user *User) {
|
|||||||
if u.MarkReadOnView != nil {
|
if u.MarkReadOnView != nil {
|
||||||
user.MarkReadOnView = *u.MarkReadOnView
|
user.MarkReadOnView = *u.MarkReadOnView
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.MarkReadOnMediaPlayerCompletion != nil {
|
|
||||||
user.MarkReadOnMediaPlayerCompletion = *u.MarkReadOnMediaPlayerCompletion
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.MediaPlaybackRate != nil {
|
|
||||||
user.MediaPlaybackRate = *u.MediaPlaybackRate
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.BlockFilterEntryRules != nil {
|
|
||||||
user.BlockFilterEntryRules = *u.BlockFilterEntryRules
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.KeepFilterEntryRules != nil {
|
|
||||||
user.KeepFilterEntryRules = *u.KeepFilterEntryRules
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UseTimezone converts last login date to the given timezone.
|
// UseTimezone converts last login date to the given timezone.
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package mediaproxy // import "miniflux.app/v2/internal/mediaproxy"
|
package proxy // import "miniflux.app/v2/internal/proxy"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"miniflux.app/v2/internal/config"
|
"miniflux.app/v2/internal/config"
|
||||||
@ -17,26 +16,31 @@ import (
|
|||||||
|
|
||||||
type urlProxyRewriter func(router *mux.Router, url string) string
|
type urlProxyRewriter func(router *mux.Router, url string) string
|
||||||
|
|
||||||
func RewriteDocumentWithRelativeProxyURL(router *mux.Router, htmlDocument string) string {
|
// ProxyRewriter replaces media URLs with internal proxy URLs.
|
||||||
return genericProxyRewriter(router, ProxifyRelativeURL, htmlDocument)
|
func ProxyRewriter(router *mux.Router, data string) string {
|
||||||
|
return genericProxyRewriter(router, ProxifyURL, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RewriteDocumentWithAbsoluteProxyURL(router *mux.Router, htmlDocument string) string {
|
// AbsoluteProxyRewriter do the same as ProxyRewriter except it uses absolute URLs.
|
||||||
return genericProxyRewriter(router, ProxifyAbsoluteURL, htmlDocument)
|
func AbsoluteProxyRewriter(router *mux.Router, host, data string) string {
|
||||||
|
proxifyFunction := func(router *mux.Router, url string) string {
|
||||||
|
return AbsoluteProxifyURL(router, host, url)
|
||||||
|
}
|
||||||
|
return genericProxyRewriter(router, proxifyFunction, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func genericProxyRewriter(router *mux.Router, proxifyFunction urlProxyRewriter, htmlDocument string) string {
|
func genericProxyRewriter(router *mux.Router, proxifyFunction urlProxyRewriter, data string) string {
|
||||||
proxyOption := config.Opts.MediaProxyMode()
|
proxyOption := config.Opts.ProxyOption()
|
||||||
if proxyOption == "none" {
|
if proxyOption == "none" {
|
||||||
return htmlDocument
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlDocument))
|
doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return htmlDocument
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, mediaType := range config.Opts.MediaProxyResourceTypes() {
|
for _, mediaType := range config.Opts.ProxyMediaTypes() {
|
||||||
switch mediaType {
|
switch mediaType {
|
||||||
case "image":
|
case "image":
|
||||||
doc.Find("img, picture source").Each(func(i int, img *goquery.Selection) {
|
doc.Find("img, picture source").Each(func(i int, img *goquery.Selection) {
|
||||||
@ -51,15 +55,13 @@ func genericProxyRewriter(router *mux.Router, proxifyFunction urlProxyRewriter,
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if !slices.Contains(config.Opts.MediaProxyResourceTypes(), "video") {
|
doc.Find("video").Each(func(i int, video *goquery.Selection) {
|
||||||
doc.Find("video").Each(func(i int, video *goquery.Selection) {
|
if posterAttrValue, ok := video.Attr("poster"); ok {
|
||||||
if posterAttrValue, ok := video.Attr("poster"); ok {
|
if shouldProxy(posterAttrValue, proxyOption) {
|
||||||
if shouldProxy(posterAttrValue, proxyOption) {
|
video.SetAttr("poster", proxifyFunction(router, posterAttrValue))
|
||||||
video.SetAttr("poster", proxifyFunction(router, posterAttrValue))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
case "audio":
|
case "audio":
|
||||||
doc.Find("audio, audio source").Each(func(i int, audio *goquery.Selection) {
|
doc.Find("audio, audio source").Each(func(i int, audio *goquery.Selection) {
|
||||||
@ -89,7 +91,7 @@ func genericProxyRewriter(router *mux.Router, proxifyFunction urlProxyRewriter,
|
|||||||
|
|
||||||
output, err := doc.Find("body").First().Html()
|
output, err := doc.Find("body").First().Html()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return htmlDocument
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
return output
|
return output
|
@ -1,7 +1,7 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package mediaproxy // import "miniflux.app/v2/internal/mediaproxy"
|
package proxy // import "miniflux.app/v2/internal/proxy"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -29,11 +29,11 @@ func TestProxyFilterWithHttpDefault(t *testing.T) {
|
|||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||||
|
|
||||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,11 +53,11 @@ func TestProxyFilterWithHttpsDefault(t *testing.T) {
|
|||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||||
|
|
||||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,11 +76,11 @@ func TestProxyFilterWithHttpNever(t *testing.T) {
|
|||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||||
|
|
||||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
expected := input
|
expected := input
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,11 +99,11 @@ func TestProxyFilterWithHttpsNever(t *testing.T) {
|
|||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||||
|
|
||||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
expected := input
|
expected := input
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,11 +124,11 @@ func TestProxyFilterWithHttpAlways(t *testing.T) {
|
|||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||||
|
|
||||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,98 +149,11 @@ func TestProxyFilterWithHttpsAlways(t *testing.T) {
|
|||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||||
|
|
||||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
expected := `<p><img src="/proxy/LdPNR1GBDigeeNp2ArUQRyZsVqT_PWLfHGjYFrrWWIY=/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
|
expected := `<p><img src="/proxy/LdPNR1GBDigeeNp2ArUQRyZsVqT_PWLfHGjYFrrWWIY=/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAbsoluteProxyFilterWithHttpsAlways(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("PROXY_OPTION", "all")
|
|
||||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
|
||||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
parser := config.NewParser()
|
|
||||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := mux.NewRouter()
|
|
||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
|
||||||
|
|
||||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
|
||||||
output := RewriteDocumentWithAbsoluteProxyURL(r, input)
|
|
||||||
expected := `<p><img src="http://localhost/proxy/LdPNR1GBDigeeNp2ArUQRyZsVqT_PWLfHGjYFrrWWIY=/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
|
|
||||||
|
|
||||||
if expected != output {
|
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAbsoluteProxyFilterWithCustomPortAndSubfolderInBaseURL(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("BASE_URL", "http://example.org:88/folder/")
|
|
||||||
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
parser := config.NewParser()
|
|
||||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Opts.BaseURL() != "http://example.org:88/folder" {
|
|
||||||
t.Fatalf(`Unexpected base URL, got "%s"`, config.Opts.BaseURL())
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Opts.RootURL() != "http://example.org:88" {
|
|
||||||
t.Fatalf(`Unexpected root URL, got "%s"`, config.Opts.RootURL())
|
|
||||||
}
|
|
||||||
|
|
||||||
router := mux.NewRouter()
|
|
||||||
|
|
||||||
if config.Opts.BasePath() != "" {
|
|
||||||
router = router.PathPrefix(config.Opts.BasePath()).Subrouter()
|
|
||||||
}
|
|
||||||
|
|
||||||
router.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
|
||||||
|
|
||||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
|
||||||
output := RewriteDocumentWithAbsoluteProxyURL(router, input)
|
|
||||||
expected := `<p><img src="http://example.org:88/folder/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
|
||||||
|
|
||||||
if expected != output {
|
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAbsoluteProxyFilterWithHttpsAlwaysAndAudioTag(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("PROXY_OPTION", "all")
|
|
||||||
os.Setenv("PROXY_MEDIA_TYPES", "audio")
|
|
||||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
parser := config.NewParser()
|
|
||||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := mux.NewRouter()
|
|
||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
|
||||||
|
|
||||||
input := `<audio src="https://website/folder/audio.mp3"></audio>`
|
|
||||||
output := RewriteDocumentWithAbsoluteProxyURL(r, input)
|
|
||||||
expected := `<audio src="http://localhost/proxy/EmBTvmU5B17wGuONkeknkptYopW_Tl6Y6_W8oYbN_Xs=/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9hdWRpby5tcDM="></audio>`
|
|
||||||
|
|
||||||
if expected != output {
|
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,61 +174,11 @@ func TestProxyFilterWithHttpsAlwaysAndCustomProxyServer(t *testing.T) {
|
|||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||||
|
|
||||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
expected := `<p><img src="https://proxy-example/proxy/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
|
expected := `<p><img src="https://proxy-example/proxy/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProxyFilterWithHttpsAlwaysAndIncorrectCustomProxyServer(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("PROXY_OPTION", "all")
|
|
||||||
os.Setenv("PROXY_MEDIA_TYPES", "image")
|
|
||||||
os.Setenv("PROXY_URL", "http://:8080example.com")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
parser := config.NewParser()
|
|
||||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := mux.NewRouter()
|
|
||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
|
||||||
|
|
||||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
|
||||||
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
|
||||||
|
|
||||||
if expected != output {
|
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAbsoluteProxyFilterWithHttpsAlwaysAndCustomProxyServer(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("MEDIA_PROXY_MODE", "all")
|
|
||||||
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
|
|
||||||
os.Setenv("MEDIA_PROXY_CUSTOM_URL", "https://proxy-example/proxy")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
parser := config.NewParser()
|
|
||||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := mux.NewRouter()
|
|
||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
|
||||||
|
|
||||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
|
||||||
output := RewriteDocumentWithAbsoluteProxyURL(r, input)
|
|
||||||
expected := `<p><img src="https://proxy-example/proxy/aHR0cHM6Ly93ZWJzaXRlL2ZvbGRlci9pbWFnZS5wbmc=" alt="Test"/></p>`
|
|
||||||
|
|
||||||
if expected != output {
|
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,11 +198,11 @@ func TestProxyFilterWithHttpInvalid(t *testing.T) {
|
|||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||||
|
|
||||||
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,11 +222,11 @@ func TestProxyFilterWithHttpsInvalid(t *testing.T) {
|
|||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
||||||
|
|
||||||
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
|
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,7 +248,7 @@ func TestProxyFilterWithSrcset(t *testing.T) {
|
|||||||
|
|
||||||
input := `<p><img src="http://website/folder/image.png" srcset="http://website/folder/image2.png 656w, http://website/folder/image3.png 360w" alt="test"></p>`
|
input := `<p><img src="http://website/folder/image.png" srcset="http://website/folder/image2.png 656w, http://website/folder/image3.png 360w" alt="test"></p>`
|
||||||
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" srcset="/proxy/aY5Hb4urDnUCly2vTJ7ExQeeaVS-52O7kjUr2v9VrAs=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, /proxy/QgAmrJWiAud_nNAsz3F8OTxaIofwAiO36EDzH_YfMzo=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMy5wbmc= 360w" alt="test"/></p>`
|
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" srcset="/proxy/aY5Hb4urDnUCly2vTJ7ExQeeaVS-52O7kjUr2v9VrAs=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, /proxy/QgAmrJWiAud_nNAsz3F8OTxaIofwAiO36EDzH_YfMzo=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMy5wbmc= 360w" alt="test"/></p>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %s`, output)
|
t.Errorf(`Not expected output: got %s`, output)
|
||||||
@ -410,7 +273,7 @@ func TestProxyFilterWithEmptySrcset(t *testing.T) {
|
|||||||
|
|
||||||
input := `<p><img src="http://website/folder/image.png" srcset="" alt="test"></p>`
|
input := `<p><img src="http://website/folder/image.png" srcset="" alt="test"></p>`
|
||||||
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" srcset="" alt="test"/></p>`
|
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" srcset="" alt="test"/></p>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %s`, output)
|
t.Errorf(`Not expected output: got %s`, output)
|
||||||
@ -435,7 +298,7 @@ func TestProxyFilterWithPictureSource(t *testing.T) {
|
|||||||
|
|
||||||
input := `<picture><source srcset="http://website/folder/image2.png 656w, http://website/folder/image3.png 360w, https://website/some,image.png 2x"></picture>`
|
input := `<picture><source srcset="http://website/folder/image2.png 656w, http://website/folder/image3.png 360w, https://website/some,image.png 2x"></picture>`
|
||||||
expected := `<picture><source srcset="/proxy/aY5Hb4urDnUCly2vTJ7ExQeeaVS-52O7kjUr2v9VrAs=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, /proxy/QgAmrJWiAud_nNAsz3F8OTxaIofwAiO36EDzH_YfMzo=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMy5wbmc= 360w, /proxy/ZIw0hv8WhSTls5aSqhnFaCXlUrKIqTnBRaY0-NaLnds=/aHR0cHM6Ly93ZWJzaXRlL3NvbWUsaW1hZ2UucG5n 2x"/></picture>`
|
expected := `<picture><source srcset="/proxy/aY5Hb4urDnUCly2vTJ7ExQeeaVS-52O7kjUr2v9VrAs=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, /proxy/QgAmrJWiAud_nNAsz3F8OTxaIofwAiO36EDzH_YfMzo=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMy5wbmc= 360w, /proxy/ZIw0hv8WhSTls5aSqhnFaCXlUrKIqTnBRaY0-NaLnds=/aHR0cHM6Ly93ZWJzaXRlL3NvbWUsaW1hZ2UucG5n 2x"/></picture>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %s`, output)
|
t.Errorf(`Not expected output: got %s`, output)
|
||||||
@ -460,7 +323,7 @@ func TestProxyFilterOnlyNonHTTPWithPictureSource(t *testing.T) {
|
|||||||
|
|
||||||
input := `<picture><source srcset="http://website/folder/image2.png 656w, https://website/some,image.png 2x"></picture>`
|
input := `<picture><source srcset="http://website/folder/image2.png 656w, https://website/some,image.png 2x"></picture>`
|
||||||
expected := `<picture><source srcset="/proxy/aY5Hb4urDnUCly2vTJ7ExQeeaVS-52O7kjUr2v9VrAs=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, https://website/some,image.png 2x"/></picture>`
|
expected := `<picture><source srcset="/proxy/aY5Hb4urDnUCly2vTJ7ExQeeaVS-52O7kjUr2v9VrAs=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlMi5wbmc= 656w, https://website/some,image.png 2x"/></picture>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %s`, output)
|
t.Errorf(`Not expected output: got %s`, output)
|
||||||
@ -484,7 +347,7 @@ func TestProxyWithImageDataURL(t *testing.T) {
|
|||||||
|
|
||||||
input := `<img src="data:image/gif;base64,test">`
|
input := `<img src="data:image/gif;base64,test">`
|
||||||
expected := `<img src="data:image/gif;base64,test"/>`
|
expected := `<img src="data:image/gif;base64,test"/>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %s`, output)
|
t.Errorf(`Not expected output: got %s`, output)
|
||||||
@ -508,7 +371,7 @@ func TestProxyWithImageSourceDataURL(t *testing.T) {
|
|||||||
|
|
||||||
input := `<picture><source srcset="data:image/gif;base64,test"/></picture>`
|
input := `<picture><source srcset="data:image/gif;base64,test"/></picture>`
|
||||||
expected := `<picture><source srcset="data:image/gif;base64,test"/></picture>`
|
expected := `<picture><source srcset="data:image/gif;base64,test"/></picture>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %s`, output)
|
t.Errorf(`Not expected output: got %s`, output)
|
||||||
@ -533,7 +396,7 @@ func TestProxyFilterWithVideo(t *testing.T) {
|
|||||||
|
|
||||||
input := `<video poster="https://example.com/img.png" src="https://example.com/video.mp4"></video>`
|
input := `<video poster="https://example.com/img.png" src="https://example.com/video.mp4"></video>`
|
||||||
expected := `<video poster="/proxy/aDFfroYL57q5XsojIzATT6OYUCkuVSPXYJQAVrotnLw=/aHR0cHM6Ly9leGFtcGxlLmNvbS9pbWcucG5n" src="/proxy/0y3LR8zlx8S8qJkj1qWFOO6x3a-5yf2gLWjGIJV5yyc=/aHR0cHM6Ly9leGFtcGxlLmNvbS92aWRlby5tcDQ="></video>`
|
expected := `<video poster="/proxy/aDFfroYL57q5XsojIzATT6OYUCkuVSPXYJQAVrotnLw=/aHR0cHM6Ly9leGFtcGxlLmNvbS9pbWcucG5n" src="/proxy/0y3LR8zlx8S8qJkj1qWFOO6x3a-5yf2gLWjGIJV5yyc=/aHR0cHM6Ly9leGFtcGxlLmNvbS92aWRlby5tcDQ="></video>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %s`, output)
|
t.Errorf(`Not expected output: got %s`, output)
|
||||||
@ -558,32 +421,7 @@ func TestProxyFilterVideoPoster(t *testing.T) {
|
|||||||
|
|
||||||
input := `<video poster="https://example.com/img.png" src="https://example.com/video.mp4"></video>`
|
input := `<video poster="https://example.com/img.png" src="https://example.com/video.mp4"></video>`
|
||||||
expected := `<video poster="/proxy/aDFfroYL57q5XsojIzATT6OYUCkuVSPXYJQAVrotnLw=/aHR0cHM6Ly9leGFtcGxlLmNvbS9pbWcucG5n" src="https://example.com/video.mp4"></video>`
|
expected := `<video poster="/proxy/aDFfroYL57q5XsojIzATT6OYUCkuVSPXYJQAVrotnLw=/aHR0cHM6Ly9leGFtcGxlLmNvbS9pbWcucG5n" src="https://example.com/video.mp4"></video>`
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
output := ProxyRewriter(r, input)
|
||||||
|
|
||||||
if expected != output {
|
|
||||||
t.Errorf(`Not expected output: got %s`, output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProxyFilterVideoPosterOnce(t *testing.T) {
|
|
||||||
os.Clearenv()
|
|
||||||
os.Setenv("PROXY_OPTION", "all")
|
|
||||||
os.Setenv("PROXY_MEDIA_TYPES", "image,video")
|
|
||||||
os.Setenv("PROXY_PRIVATE_KEY", "test")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
parser := config.NewParser()
|
|
||||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`Parsing failure: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := mux.NewRouter()
|
|
||||||
r.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
|
|
||||||
|
|
||||||
input := `<video poster="https://example.com/img.png" src="https://example.com/video.mp4"></video>`
|
|
||||||
expected := `<video poster="/proxy/aDFfroYL57q5XsojIzATT6OYUCkuVSPXYJQAVrotnLw=/aHR0cHM6Ly9leGFtcGxlLmNvbS9pbWcucG5n" src="/proxy/0y3LR8zlx8S8qJkj1qWFOO6x3a-5yf2gLWjGIJV5yyc=/aHR0cHM6Ly9leGFtcGxlLmNvbS92aWRlby5tcDQ="></video>`
|
|
||||||
output := RewriteDocumentWithRelativeProxyURL(r, input)
|
|
||||||
|
|
||||||
if expected != output {
|
if expected != output {
|
||||||
t.Errorf(`Not expected output: got %s`, output)
|
t.Errorf(`Not expected output: got %s`, output)
|
69
internal/proxy/proxy.go
Normal file
69
internal/proxy/proxy.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package proxy // import "miniflux.app/v2/internal/proxy"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"miniflux.app/v2/internal/http/route"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"miniflux.app/v2/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProxifyURL generates a relative URL for a proxified resource.
|
||||||
|
func ProxifyURL(router *mux.Router, link string) string {
|
||||||
|
if link != "" {
|
||||||
|
proxyImageUrl := config.Opts.ProxyUrl()
|
||||||
|
|
||||||
|
if proxyImageUrl == "" {
|
||||||
|
mac := hmac.New(sha256.New, config.Opts.ProxyPrivateKey())
|
||||||
|
mac.Write([]byte(link))
|
||||||
|
digest := mac.Sum(nil)
|
||||||
|
return route.Path(router, "proxy", "encodedDigest", base64.URLEncoding.EncodeToString(digest), "encodedURL", base64.URLEncoding.EncodeToString([]byte(link)))
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyUrl, err := url.Parse(proxyImageUrl)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyUrl.Path = path.Join(proxyUrl.Path, base64.URLEncoding.EncodeToString([]byte(link)))
|
||||||
|
return proxyUrl.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbsoluteProxifyURL generates an absolute URL for a proxified resource.
|
||||||
|
func AbsoluteProxifyURL(router *mux.Router, host, link string) string {
|
||||||
|
if link != "" {
|
||||||
|
proxyImageUrl := config.Opts.ProxyUrl()
|
||||||
|
|
||||||
|
if proxyImageUrl == "" {
|
||||||
|
mac := hmac.New(sha256.New, config.Opts.ProxyPrivateKey())
|
||||||
|
mac.Write([]byte(link))
|
||||||
|
digest := mac.Sum(nil)
|
||||||
|
path := route.Path(router, "proxy", "encodedDigest", base64.URLEncoding.EncodeToString(digest), "encodedURL", base64.URLEncoding.EncodeToString([]byte(link)))
|
||||||
|
if config.Opts.HTTPS {
|
||||||
|
return "https://" + host + path
|
||||||
|
} else {
|
||||||
|
return "http://" + host + path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyUrl, err := url.Parse(proxyImageUrl)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyUrl.Path = path.Join(proxyUrl.Path, base64.URLEncoding.EncodeToString([]byte(link)))
|
||||||
|
return proxyUrl.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
@ -6,114 +6,158 @@ package atom // import "miniflux.app/v2/internal/reader/atom"
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"html"
|
"html"
|
||||||
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"miniflux.app/v2/internal/crypto"
|
||||||
|
"miniflux.app/v2/internal/model"
|
||||||
|
"miniflux.app/v2/internal/reader/date"
|
||||||
|
"miniflux.app/v2/internal/reader/sanitizer"
|
||||||
|
"miniflux.app/v2/internal/urllib"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Specs: http://web.archive.org/web/20060811235523/http://www.mnot.net/drafts/draft-nottingham-atom-format-02.html
|
// Specs: http://web.archive.org/web/20060811235523/http://www.mnot.net/drafts/draft-nottingham-atom-format-02.html
|
||||||
type Atom03Feed struct {
|
type atom03Feed struct {
|
||||||
Version string `xml:"version,attr"`
|
ID string `xml:"id"`
|
||||||
|
Title atom03Text `xml:"title"`
|
||||||
// The "atom:id" element's content conveys a permanent, globally unique identifier for the feed.
|
Author atomPerson `xml:"author"`
|
||||||
// It MUST NOT change over time, even if the feed is relocated. atom:feed elements MAY contain an atom:id element,
|
Links atomLinks `xml:"link"`
|
||||||
// but MUST NOT contain more than one. The content of this element, when present, MUST be a URI.
|
Entries []atom03Entry `xml:"entry"`
|
||||||
ID string `xml:"http://purl.org/atom/ns# id"`
|
|
||||||
|
|
||||||
// The "atom:title" element is a Content construct that conveys a human-readable title for the feed.
|
|
||||||
// atom:feed elements MUST contain exactly one atom:title element.
|
|
||||||
// If the feed describes a Web resource, its content SHOULD be the same as that resource's title.
|
|
||||||
Title Atom03Content `xml:"http://purl.org/atom/ns# title"`
|
|
||||||
|
|
||||||
// The "atom:link" element is a Link construct that conveys a URI associated with the feed.
|
|
||||||
// The nature of the relationship as well as the link itself is determined by the element's content.
|
|
||||||
// atom:feed elements MUST contain at least one atom:link element with a rel attribute value of "alternate".
|
|
||||||
// atom:feed elements MUST NOT contain more than one atom:link element with a rel attribute value of "alternate" that has the same type attribute value.
|
|
||||||
// atom:feed elements MAY contain additional atom:link elements beyond those described above.
|
|
||||||
Links AtomLinks `xml:"http://purl.org/atom/ns# link"`
|
|
||||||
|
|
||||||
// The "atom:author" element is a Person construct that indicates the default author of the feed.
|
|
||||||
// atom:feed elements MUST contain exactly one atom:author element,
|
|
||||||
// UNLESS all of the atom:feed element's child atom:entry elements contain an atom:author element.
|
|
||||||
// atom:feed elements MUST NOT contain more than one atom:author element.
|
|
||||||
Author AtomPerson `xml:"http://purl.org/atom/ns# author"`
|
|
||||||
|
|
||||||
// The "atom:entry" element's represents an individual entry that is contained by the feed.
|
|
||||||
// atom:feed elements MAY contain one or more atom:entry elements.
|
|
||||||
Entries []Atom03Entry `xml:"http://purl.org/atom/ns# entry"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Atom03Entry struct {
|
func (a *atom03Feed) Transform(baseURL string) *model.Feed {
|
||||||
// The "atom:id" element's content conveys a permanent, globally unique identifier for the entry.
|
var err error
|
||||||
// It MUST NOT change over time, even if other representations of the entry (such as a web representation pointed to by the entry's atom:link element) are relocated.
|
|
||||||
// If the same entry is syndicated in two atom:feeds published by the same entity, the entry's atom:id MUST be the same in both feeds.
|
|
||||||
ID string `xml:"id"`
|
|
||||||
|
|
||||||
// The "atom:title" element is a Content construct that conveys a human-readable title for the entry.
|
feed := new(model.Feed)
|
||||||
// atom:entry elements MUST have exactly one "atom:title" element.
|
|
||||||
// If an entry describes a Web resource, its content SHOULD be the same as that resource's title.
|
|
||||||
Title Atom03Content `xml:"title"`
|
|
||||||
|
|
||||||
// The "atom:modified" element is a Date construct that indicates the time that the entry was last modified.
|
feedURL := a.Links.firstLinkWithRelation("self")
|
||||||
// atom:entry elements MUST contain an atom:modified element, but MUST NOT contain more than one.
|
feed.FeedURL, err = urllib.AbsoluteURL(baseURL, feedURL)
|
||||||
// The content of an atom:modified element MUST have a time zone whose value SHOULD be "UTC".
|
if err != nil {
|
||||||
Modified string `xml:"modified"`
|
feed.FeedURL = feedURL
|
||||||
|
}
|
||||||
|
|
||||||
// The "atom:issued" element is a Date construct that indicates the time that the entry was issued.
|
siteURL := a.Links.originalLink()
|
||||||
// atom:entry elements MUST contain an atom:issued element, but MUST NOT contain more than one.
|
feed.SiteURL, err = urllib.AbsoluteURL(baseURL, siteURL)
|
||||||
// The content of an atom:issued element MAY omit a time zone.
|
if err != nil {
|
||||||
Issued string `xml:"issued"`
|
feed.SiteURL = siteURL
|
||||||
|
}
|
||||||
|
|
||||||
// The "atom:created" element is a Date construct that indicates the time that the entry was created.
|
feed.Title = a.Title.String()
|
||||||
// atom:entry elements MAY contain an atom:created element, but MUST NOT contain more than one.
|
if feed.Title == "" {
|
||||||
// The content of an atom:created element MUST have a time zone whose value SHOULD be "UTC".
|
feed.Title = feed.SiteURL
|
||||||
// If atom:created is not present, its content MUST considered to be the same as that of atom:modified.
|
}
|
||||||
Created string `xml:"created"`
|
|
||||||
|
|
||||||
// The "atom:link" element is a Link construct that conveys a URI associated with the entry.
|
for _, entry := range a.Entries {
|
||||||
// The nature of the relationship as well as the link itself is determined by the element's content.
|
item := entry.Transform()
|
||||||
// atom:entry elements MUST contain at least one atom:link element with a rel attribute value of "alternate".
|
entryURL, err := urllib.AbsoluteURL(feed.SiteURL, item.URL)
|
||||||
// atom:entry elements MUST NOT contain more than one atom:link element with a rel attribute value of "alternate" that has the same type attribute value.
|
if err == nil {
|
||||||
// atom:entry elements MAY contain additional atom:link elements beyond those described above.
|
item.URL = entryURL
|
||||||
Links AtomLinks `xml:"link"`
|
}
|
||||||
|
|
||||||
// The "atom:summary" element is a Content construct that conveys a short summary, abstract or excerpt of the entry.
|
if item.Author == "" {
|
||||||
// atom:entry elements MAY contain an atom:created element, but MUST NOT contain more than one.
|
item.Author = a.Author.String()
|
||||||
Summary Atom03Content `xml:"summary"`
|
}
|
||||||
|
|
||||||
// The "atom:content" element is a Content construct that conveys the content of the entry.
|
if item.Title == "" {
|
||||||
// atom:entry elements MAY contain one or more atom:content elements.
|
item.Title = sanitizer.TruncateHTML(item.Content, 100)
|
||||||
Content Atom03Content `xml:"content"`
|
}
|
||||||
|
|
||||||
// The "atom:author" element is a Person construct that indicates the default author of the entry.
|
if item.Title == "" {
|
||||||
// atom:entry elements MUST contain exactly one atom:author element,
|
item.Title = item.URL
|
||||||
// UNLESS the atom:feed element containing them contains an atom:author element itself.
|
}
|
||||||
// atom:entry elements MUST NOT contain more than one atom:author element.
|
|
||||||
Author AtomPerson `xml:"author"`
|
feed.Entries = append(feed.Entries, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return feed
|
||||||
}
|
}
|
||||||
|
|
||||||
type Atom03Content struct {
|
type atom03Entry struct {
|
||||||
// Content constructs MAY have a "type" attribute, whose value indicates the media type of the content.
|
ID string `xml:"id"`
|
||||||
// When present, this attribute's value MUST be a registered media type [RFC2045].
|
Title atom03Text `xml:"title"`
|
||||||
// If not present, its value MUST be considered to be "text/plain".
|
Modified string `xml:"modified"`
|
||||||
Type string `xml:"type,attr"`
|
Issued string `xml:"issued"`
|
||||||
|
Created string `xml:"created"`
|
||||||
|
Links atomLinks `xml:"link"`
|
||||||
|
Summary atom03Text `xml:"summary"`
|
||||||
|
Content atom03Text `xml:"content"`
|
||||||
|
Author atomPerson `xml:"author"`
|
||||||
|
}
|
||||||
|
|
||||||
// Content constructs MAY have a "mode" attribute, whose value indicates the method used to encode the content.
|
func (a *atom03Entry) Transform() *model.Entry {
|
||||||
// When present, this attribute's value MUST be listed below.
|
entry := model.NewEntry()
|
||||||
// If not present, its value MUST be considered to be "xml".
|
entry.URL = a.Links.originalLink()
|
||||||
//
|
entry.Date = a.entryDate()
|
||||||
// "xml": A mode attribute with the value "xml" indicates that the element's content is inline xml (for example, namespace-qualified XHTML).
|
entry.Author = a.Author.String()
|
||||||
//
|
entry.Hash = a.entryHash()
|
||||||
// "escaped": A mode attribute with the value "escaped" indicates that the element's content is an escaped string.
|
entry.Content = a.entryContent()
|
||||||
// Processors MUST unescape the element's content before considering it as content of the indicated media type.
|
entry.Title = a.entryTitle()
|
||||||
//
|
return entry
|
||||||
// "base64": A mode attribute with the value "base64" indicates that the element's content is base64-encoded [RFC2045].
|
}
|
||||||
// Processors MUST decode the element's content before considering it as content of the the indicated media type.
|
|
||||||
Mode string `xml:"mode,attr"`
|
|
||||||
|
|
||||||
|
func (a *atom03Entry) entryTitle() string {
|
||||||
|
return sanitizer.StripTags(a.Title.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *atom03Entry) entryContent() string {
|
||||||
|
content := a.Content.String()
|
||||||
|
if content != "" {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := a.Summary.String()
|
||||||
|
if summary != "" {
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *atom03Entry) entryDate() time.Time {
|
||||||
|
dateText := ""
|
||||||
|
for _, value := range []string{a.Issued, a.Modified, a.Created} {
|
||||||
|
if value != "" {
|
||||||
|
dateText = value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dateText != "" {
|
||||||
|
result, err := date.Parse(dateText)
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug("Unable to parse date from Atom 0.3 feed",
|
||||||
|
slog.String("date", dateText),
|
||||||
|
slog.String("id", a.ID),
|
||||||
|
slog.Any("error", err),
|
||||||
|
)
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *atom03Entry) entryHash() string {
|
||||||
|
for _, value := range []string{a.ID, a.Links.originalLink()} {
|
||||||
|
if value != "" {
|
||||||
|
return crypto.Hash(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type atom03Text struct {
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
Mode string `xml:"mode,attr"`
|
||||||
CharData string `xml:",chardata"`
|
CharData string `xml:",chardata"`
|
||||||
InnerXML string `xml:",innerxml"`
|
InnerXML string `xml:",innerxml"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Atom03Content) Content() string {
|
func (a *atom03Text) String() string {
|
||||||
content := ""
|
content := ""
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
@ -1,115 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package atom // import "miniflux.app/v2/internal/reader/atom"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"miniflux.app/v2/internal/crypto"
|
|
||||||
"miniflux.app/v2/internal/model"
|
|
||||||
"miniflux.app/v2/internal/reader/date"
|
|
||||||
"miniflux.app/v2/internal/reader/sanitizer"
|
|
||||||
"miniflux.app/v2/internal/urllib"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Atom03Adapter struct {
|
|
||||||
atomFeed *Atom03Feed
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAtom03Adapter(atomFeed *Atom03Feed) *Atom03Adapter {
|
|
||||||
return &Atom03Adapter{atomFeed}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Atom03Adapter) BuildFeed(baseURL string) *model.Feed {
|
|
||||||
feed := new(model.Feed)
|
|
||||||
|
|
||||||
// Populate the feed URL.
|
|
||||||
feedURL := a.atomFeed.Links.firstLinkWithRelation("self")
|
|
||||||
if feedURL != "" {
|
|
||||||
if absoluteFeedURL, err := urllib.AbsoluteURL(baseURL, feedURL); err == nil {
|
|
||||||
feed.FeedURL = absoluteFeedURL
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
feed.FeedURL = baseURL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the site URL.
|
|
||||||
siteURL := a.atomFeed.Links.OriginalLink()
|
|
||||||
if siteURL != "" {
|
|
||||||
if absoluteSiteURL, err := urllib.AbsoluteURL(baseURL, siteURL); err == nil {
|
|
||||||
feed.SiteURL = absoluteSiteURL
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
feed.SiteURL = baseURL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the feed title.
|
|
||||||
feed.Title = a.atomFeed.Title.Content()
|
|
||||||
if feed.Title == "" {
|
|
||||||
feed.Title = feed.SiteURL
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, atomEntry := range a.atomFeed.Entries {
|
|
||||||
entry := model.NewEntry()
|
|
||||||
|
|
||||||
// Populate the entry URL.
|
|
||||||
entry.URL = atomEntry.Links.OriginalLink()
|
|
||||||
if entry.URL != "" {
|
|
||||||
if absoluteEntryURL, err := urllib.AbsoluteURL(feed.SiteURL, entry.URL); err == nil {
|
|
||||||
entry.URL = absoluteEntryURL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the entry content.
|
|
||||||
entry.Content = atomEntry.Content.Content()
|
|
||||||
if entry.Content == "" {
|
|
||||||
entry.Content = atomEntry.Summary.Content()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the entry title.
|
|
||||||
entry.Title = atomEntry.Title.Content()
|
|
||||||
if entry.Title == "" {
|
|
||||||
entry.Title = sanitizer.TruncateHTML(entry.Content, 100)
|
|
||||||
}
|
|
||||||
if entry.Title == "" {
|
|
||||||
entry.Title = entry.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the entry author.
|
|
||||||
entry.Author = atomEntry.Author.PersonName()
|
|
||||||
if entry.Author == "" {
|
|
||||||
entry.Author = a.atomFeed.Author.PersonName()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the entry date.
|
|
||||||
for _, value := range []string{atomEntry.Issued, atomEntry.Modified, atomEntry.Created} {
|
|
||||||
if parsedDate, err := date.Parse(value); err == nil {
|
|
||||||
entry.Date = parsedDate
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
slog.Debug("Unable to parse date from Atom 0.3 feed",
|
|
||||||
slog.String("date", value),
|
|
||||||
slog.String("id", atomEntry.ID),
|
|
||||||
slog.Any("error", err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if entry.Date.IsZero() {
|
|
||||||
entry.Date = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the entry hash.
|
|
||||||
for _, value := range []string{atomEntry.ID, atomEntry.Links.OriginalLink()} {
|
|
||||||
if value != "" {
|
|
||||||
entry.Hash = crypto.Hash(value)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
feed.Entries = append(feed.Entries, entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
return feed
|
|
||||||
}
|
|
@ -27,7 +27,7 @@ func TestParseAtom03(t *testing.T) {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>`
|
</feed>`
|
||||||
|
|
||||||
feed, err := Parse("http://diveintomark.org/atom.xml", bytes.NewReader([]byte(data)), "0.3")
|
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@ func TestParseAtom03(t *testing.T) {
|
|||||||
t.Errorf("Incorrect title, got: %s", feed.Title)
|
t.Errorf("Incorrect title, got: %s", feed.Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
if feed.FeedURL != "http://diveintomark.org/atom.xml" {
|
if feed.FeedURL != "http://diveintomark.org/" {
|
||||||
t.Errorf("Incorrect feed URL, got: %s", feed.FeedURL)
|
t.Errorf("Incorrect feed URL, got: %s", feed.FeedURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,28 +74,6 @@ func TestParseAtom03(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseAtom03WithoutSiteURL(t *testing.T) {
|
|
||||||
data := `<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<feed version="0.3" xmlns="http://purl.org/atom/ns#">
|
|
||||||
<modified>2003-12-13T18:30:02Z</modified>
|
|
||||||
<author><name>Mark Pilgrim</name></author>
|
|
||||||
<entry>
|
|
||||||
<title>Atom 0.3 snapshot</title>
|
|
||||||
<link rel="alternate" type="text/html" href="http://diveintomark.org/2003/12/13/atom03"/>
|
|
||||||
<id>tag:diveintomark.org,2003:3.2397</id>
|
|
||||||
</entry>
|
|
||||||
</feed>`
|
|
||||||
|
|
||||||
feed, err := Parse("http://diveintomark.org/atom.xml", bytes.NewReader([]byte(data)), "0.3")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if feed.SiteURL != "http://diveintomark.org/atom.xml" {
|
|
||||||
t.Errorf("Incorrect title, got: %s", feed.Title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseAtom03WithoutFeedTitle(t *testing.T) {
|
func TestParseAtom03WithoutFeedTitle(t *testing.T) {
|
||||||
data := `<?xml version="1.0" encoding="utf-8"?>
|
data := `<?xml version="1.0" encoding="utf-8"?>
|
||||||
<feed version="0.3" xmlns="http://purl.org/atom/ns#">
|
<feed version="0.3" xmlns="http://purl.org/atom/ns#">
|
||||||
@ -109,7 +87,7 @@ func TestParseAtom03WithoutFeedTitle(t *testing.T) {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>`
|
</feed>`
|
||||||
|
|
||||||
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)), "0.3")
|
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -132,7 +110,7 @@ func TestParseAtom03WithoutEntryTitleButWithLink(t *testing.T) {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>`
|
</feed>`
|
||||||
|
|
||||||
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)), "0.3")
|
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -160,7 +138,7 @@ func TestParseAtom03WithoutEntryTitleButWithSummary(t *testing.T) {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>`
|
</feed>`
|
||||||
|
|
||||||
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)), "0.3")
|
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -188,7 +166,7 @@ func TestParseAtom03WithoutEntryTitleButWithXMLContent(t *testing.T) {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>`
|
</feed>`
|
||||||
|
|
||||||
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)), "0.3")
|
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -219,7 +197,7 @@ func TestParseAtom03WithSummaryOnly(t *testing.T) {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>`
|
</feed>`
|
||||||
|
|
||||||
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)), "0.3")
|
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -250,7 +228,7 @@ func TestParseAtom03WithXMLContent(t *testing.T) {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>`
|
</feed>`
|
||||||
|
|
||||||
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)), "0.3")
|
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -281,7 +259,7 @@ func TestParseAtom03WithBase64Content(t *testing.T) {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>`
|
</feed>`
|
||||||
|
|
||||||
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)), "0.3")
|
feed, err := Parse("http://diveintomark.org/", bytes.NewReader([]byte(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -6,200 +6,286 @@ package atom // import "miniflux.app/v2/internal/reader/atom"
|
|||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"html"
|
"html"
|
||||||
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"miniflux.app/v2/internal/crypto"
|
||||||
|
"miniflux.app/v2/internal/model"
|
||||||
|
"miniflux.app/v2/internal/reader/date"
|
||||||
"miniflux.app/v2/internal/reader/media"
|
"miniflux.app/v2/internal/reader/media"
|
||||||
"miniflux.app/v2/internal/reader/sanitizer"
|
"miniflux.app/v2/internal/reader/sanitizer"
|
||||||
|
"miniflux.app/v2/internal/urllib"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The "atom:feed" element is the document (i.e., top-level) element of
|
|
||||||
// an Atom Feed Document, acting as a container for metadata and data
|
|
||||||
// associated with the feed. Its element children consist of metadata
|
|
||||||
// elements followed by zero or more atom:entry child elements.
|
|
||||||
//
|
|
||||||
// Specs:
|
// Specs:
|
||||||
// https://tools.ietf.org/html/rfc4287
|
// https://tools.ietf.org/html/rfc4287
|
||||||
// https://validator.w3.org/feed/docs/atom.html
|
// https://validator.w3.org/feed/docs/atom.html
|
||||||
type Atom10Feed struct {
|
type atom10Feed struct {
|
||||||
XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"`
|
XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"`
|
||||||
|
ID string `xml:"id"`
|
||||||
// The "atom:id" element conveys a permanent, universally unique
|
Title atom10Text `xml:"title"`
|
||||||
// identifier for an entry or feed.
|
Authors atomAuthors `xml:"author"`
|
||||||
//
|
Icon string `xml:"icon"`
|
||||||
// Its content MUST be an IRI, as defined by [RFC3987]. Note that the
|
Links atomLinks `xml:"link"`
|
||||||
// definition of "IRI" excludes relative references. Though the IRI
|
Entries []atom10Entry `xml:"entry"`
|
||||||
// might use a dereferencable scheme, Atom Processors MUST NOT assume it
|
|
||||||
// can be dereferenced.
|
|
||||||
//
|
|
||||||
// atom:feed elements MUST contain exactly one atom:id element.
|
|
||||||
ID string `xml:"http://www.w3.org/2005/Atom id"`
|
|
||||||
|
|
||||||
// The "atom:title" element is a Text construct that conveys a human-
|
|
||||||
// readable title for an entry or feed.
|
|
||||||
//
|
|
||||||
// atom:feed elements MUST contain exactly one atom:title element.
|
|
||||||
Title Atom10Text `xml:"http://www.w3.org/2005/Atom title"`
|
|
||||||
|
|
||||||
// The "atom:author" element is a Person construct that indicates the
|
|
||||||
// author of the entry or feed.
|
|
||||||
//
|
|
||||||
// atom:feed elements MUST contain one or more atom:author elements,
|
|
||||||
// unless all of the atom:feed element's child atom:entry elements
|
|
||||||
// contain at least one atom:author element.
|
|
||||||
Authors AtomPersons `xml:"http://www.w3.org/2005/Atom author"`
|
|
||||||
|
|
||||||
// The "atom:icon" element's content is an IRI reference [RFC3987] that
|
|
||||||
// identifies an image that provides iconic visual identification for a
|
|
||||||
// feed.
|
|
||||||
//
|
|
||||||
// atom:feed elements MUST NOT contain more than one atom:icon element.
|
|
||||||
Icon string `xml:"http://www.w3.org/2005/Atom icon"`
|
|
||||||
|
|
||||||
// The "atom:logo" element's content is an IRI reference [RFC3987] that
|
|
||||||
// identifies an image that provides visual identification for a feed.
|
|
||||||
//
|
|
||||||
// atom:feed elements MUST NOT contain more than one atom:logo element.
|
|
||||||
Logo string `xml:"http://www.w3.org/2005/Atom logo"`
|
|
||||||
|
|
||||||
// atom:feed elements SHOULD contain one atom:link element with a rel
|
|
||||||
// attribute value of "self". This is the preferred URI for
|
|
||||||
// retrieving Atom Feed Documents representing this Atom feed.
|
|
||||||
//
|
|
||||||
// atom:feed elements MUST NOT contain more than one atom:link
|
|
||||||
// element with a rel attribute value of "alternate" that has the
|
|
||||||
// same combination of type and hreflang attribute values.
|
|
||||||
Links AtomLinks `xml:"http://www.w3.org/2005/Atom link"`
|
|
||||||
|
|
||||||
// The "atom:category" element conveys information about a category
|
|
||||||
// associated with an entry or feed. This specification assigns no
|
|
||||||
// meaning to the content (if any) of this element.
|
|
||||||
//
|
|
||||||
// atom:feed elements MAY contain any number of atom:category
|
|
||||||
// elements.
|
|
||||||
Categories AtomCategories `xml:"http://www.w3.org/2005/Atom category"`
|
|
||||||
|
|
||||||
Entries []Atom10Entry `xml:"http://www.w3.org/2005/Atom entry"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Atom10Entry struct {
|
func (a *atom10Feed) Transform(baseURL string) *model.Feed {
|
||||||
// The "atom:id" element conveys a permanent, universally unique
|
var err error
|
||||||
// identifier for an entry or feed.
|
|
||||||
//
|
|
||||||
// Its content MUST be an IRI, as defined by [RFC3987]. Note that the
|
|
||||||
// definition of "IRI" excludes relative references. Though the IRI
|
|
||||||
// might use a dereferencable scheme, Atom Processors MUST NOT assume it
|
|
||||||
// can be dereferenced.
|
|
||||||
//
|
|
||||||
// atom:entry elements MUST contain exactly one atom:id element.
|
|
||||||
ID string `xml:"http://www.w3.org/2005/Atom id"`
|
|
||||||
|
|
||||||
// The "atom:title" element is a Text construct that conveys a human-
|
feed := new(model.Feed)
|
||||||
// readable title for an entry or feed.
|
|
||||||
//
|
|
||||||
// atom:entry elements MUST contain exactly one atom:title element.
|
|
||||||
Title Atom10Text `xml:"http://www.w3.org/2005/Atom title"`
|
|
||||||
|
|
||||||
// The "atom:published" element is a Date construct indicating an
|
feedURL := a.Links.firstLinkWithRelation("self")
|
||||||
// instant in time associated with an event early in the life cycle of
|
feed.FeedURL, err = urllib.AbsoluteURL(baseURL, feedURL)
|
||||||
// the entry.
|
if err != nil {
|
||||||
Published string `xml:"http://www.w3.org/2005/Atom published"`
|
feed.FeedURL = feedURL
|
||||||
|
}
|
||||||
|
|
||||||
// The "atom:updated" element is a Date construct indicating the most
|
siteURL := a.Links.originalLink()
|
||||||
// recent instant in time when an entry or feed was modified in a way
|
feed.SiteURL, err = urllib.AbsoluteURL(baseURL, siteURL)
|
||||||
// the publisher considers significant. Therefore, not all
|
if err != nil {
|
||||||
// modifications necessarily result in a changed atom:updated value.
|
feed.SiteURL = siteURL
|
||||||
//
|
}
|
||||||
// atom:entry elements MUST contain exactly one atom:updated element.
|
|
||||||
Updated string `xml:"http://www.w3.org/2005/Atom updated"`
|
|
||||||
|
|
||||||
// atom:entry elements MUST NOT contain more than one atom:link
|
feed.Title = html.UnescapeString(a.Title.String())
|
||||||
// element with a rel attribute value of "alternate" that has the
|
if feed.Title == "" {
|
||||||
// same combination of type and hreflang attribute values.
|
feed.Title = feed.SiteURL
|
||||||
Links AtomLinks `xml:"http://www.w3.org/2005/Atom link"`
|
}
|
||||||
|
|
||||||
// atom:entry elements MUST contain an atom:summary element in either
|
feed.IconURL = strings.TrimSpace(a.Icon)
|
||||||
// of the following cases:
|
|
||||||
// * the atom:entry contains an atom:content that has a "src"
|
|
||||||
// attribute (and is thus empty).
|
|
||||||
// * the atom:entry contains content that is encoded in Base64;
|
|
||||||
// i.e., the "type" attribute of atom:content is a MIME media type
|
|
||||||
// [MIMEREG], but is not an XML media type [RFC3023], does not
|
|
||||||
// begin with "text/", and does not end with "/xml" or "+xml".
|
|
||||||
//
|
|
||||||
// atom:entry elements MUST NOT contain more than one atom:summary
|
|
||||||
// element.
|
|
||||||
Summary Atom10Text `xml:"http://www.w3.org/2005/Atom summary"`
|
|
||||||
|
|
||||||
// atom:entry elements MUST NOT contain more than one atom:content
|
for _, entry := range a.Entries {
|
||||||
// element.
|
item := entry.Transform()
|
||||||
Content Atom10Text `xml:"http://www.w3.org/2005/Atom content"`
|
entryURL, err := urllib.AbsoluteURL(feed.SiteURL, item.URL)
|
||||||
|
if err == nil {
|
||||||
|
item.URL = entryURL
|
||||||
|
}
|
||||||
|
|
||||||
// The "atom:author" element is a Person construct that indicates the
|
if item.Author == "" {
|
||||||
// author of the entry or feed.
|
item.Author = a.Authors.String()
|
||||||
//
|
}
|
||||||
// atom:entry elements MUST contain one or more atom:author elements
|
|
||||||
Authors AtomPersons `xml:"http://www.w3.org/2005/Atom author"`
|
|
||||||
|
|
||||||
// The "atom:category" element conveys information about a category
|
if item.Title == "" {
|
||||||
// associated with an entry or feed. This specification assigns no
|
item.Title = sanitizer.TruncateHTML(item.Content, 100)
|
||||||
// meaning to the content (if any) of this element.
|
}
|
||||||
//
|
|
||||||
// atom:entry elements MAY contain any number of atom:category
|
|
||||||
// elements.
|
|
||||||
Categories AtomCategories `xml:"http://www.w3.org/2005/Atom category"`
|
|
||||||
|
|
||||||
media.MediaItemElement
|
if item.Title == "" {
|
||||||
|
item.Title = item.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
feed.Entries = append(feed.Entries, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return feed
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Text construct contains human-readable text, usually in small
|
type atom10Entry struct {
|
||||||
// quantities. The content of Text constructs is Language-Sensitive.
|
ID string `xml:"id"`
|
||||||
// Specs: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1
|
Title atom10Text `xml:"title"`
|
||||||
// Text: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.1
|
Published string `xml:"published"`
|
||||||
// HTML: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.2
|
Updated string `xml:"updated"`
|
||||||
// XHTML: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.3
|
Links atomLinks `xml:"link"`
|
||||||
type Atom10Text struct {
|
Summary atom10Text `xml:"summary"`
|
||||||
|
Content atom10Text `xml:"http://www.w3.org/2005/Atom content"`
|
||||||
|
Authors atomAuthors `xml:"author"`
|
||||||
|
Categories []atom10Category `xml:"category"`
|
||||||
|
media.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *atom10Entry) Transform() *model.Entry {
|
||||||
|
entry := model.NewEntry()
|
||||||
|
entry.URL = a.Links.originalLink()
|
||||||
|
entry.Date = a.entryDate()
|
||||||
|
entry.Author = a.Authors.String()
|
||||||
|
entry.Hash = a.entryHash()
|
||||||
|
entry.Content = a.entryContent()
|
||||||
|
entry.Title = a.entryTitle()
|
||||||
|
entry.Enclosures = a.entryEnclosures()
|
||||||
|
entry.CommentsURL = a.entryCommentsURL()
|
||||||
|
entry.Tags = a.entryCategories()
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *atom10Entry) entryTitle() string {
|
||||||
|
return html.UnescapeString(a.Title.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *atom10Entry) entryContent() string {
|
||||||
|
content := a.Content.String()
|
||||||
|
if content != "" {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := a.Summary.String()
|
||||||
|
if summary != "" {
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaDescription := a.FirstMediaDescription()
|
||||||
|
if mediaDescription != "" {
|
||||||
|
return mediaDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The published date represents the original creation date for YouTube feeds.
|
||||||
|
// Example:
|
||||||
|
// <published>2019-01-26T08:02:28+00:00</published>
|
||||||
|
// <updated>2019-01-29T07:27:27+00:00</updated>
|
||||||
|
func (a *atom10Entry) entryDate() time.Time {
|
||||||
|
dateText := a.Published
|
||||||
|
if dateText == "" {
|
||||||
|
dateText = a.Updated
|
||||||
|
}
|
||||||
|
|
||||||
|
if dateText != "" {
|
||||||
|
result, err := date.Parse(dateText)
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug("Unable to parse date from Atom 0.3 feed",
|
||||||
|
slog.String("date", dateText),
|
||||||
|
slog.String("id", a.ID),
|
||||||
|
slog.Any("error", err),
|
||||||
|
)
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *atom10Entry) entryHash() string {
|
||||||
|
for _, value := range []string{a.ID, a.Links.originalLink()} {
|
||||||
|
if value != "" {
|
||||||
|
return crypto.Hash(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *atom10Entry) entryEnclosures() model.EnclosureList {
|
||||||
|
enclosures := make(model.EnclosureList, 0)
|
||||||
|
duplicates := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, mediaThumbnail := range a.AllMediaThumbnails() {
|
||||||
|
if _, found := duplicates[mediaThumbnail.URL]; !found {
|
||||||
|
duplicates[mediaThumbnail.URL] = true
|
||||||
|
enclosures = append(enclosures, &model.Enclosure{
|
||||||
|
URL: mediaThumbnail.URL,
|
||||||
|
MimeType: mediaThumbnail.MimeType(),
|
||||||
|
Size: mediaThumbnail.Size(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, link := range a.Links {
|
||||||
|
if strings.EqualFold(link.Rel, "enclosure") {
|
||||||
|
if link.URL == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := duplicates[link.URL]; !found {
|
||||||
|
duplicates[link.URL] = true
|
||||||
|
length, _ := strconv.ParseInt(link.Length, 10, 0)
|
||||||
|
enclosures = append(enclosures, &model.Enclosure{URL: link.URL, MimeType: link.Type, Size: length})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mediaContent := range a.AllMediaContents() {
|
||||||
|
if _, found := duplicates[mediaContent.URL]; !found {
|
||||||
|
duplicates[mediaContent.URL] = true
|
||||||
|
enclosures = append(enclosures, &model.Enclosure{
|
||||||
|
URL: mediaContent.URL,
|
||||||
|
MimeType: mediaContent.MimeType(),
|
||||||
|
Size: mediaContent.Size(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mediaPeerLink := range a.AllMediaPeerLinks() {
|
||||||
|
if _, found := duplicates[mediaPeerLink.URL]; !found {
|
||||||
|
duplicates[mediaPeerLink.URL] = true
|
||||||
|
enclosures = append(enclosures, &model.Enclosure{
|
||||||
|
URL: mediaPeerLink.URL,
|
||||||
|
MimeType: mediaPeerLink.MimeType(),
|
||||||
|
Size: mediaPeerLink.Size(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return enclosures
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *atom10Entry) entryCategories() []string {
|
||||||
|
categoryList := make([]string, 0)
|
||||||
|
|
||||||
|
for _, atomCategory := range r.Categories {
|
||||||
|
if strings.TrimSpace(atomCategory.Label) != "" {
|
||||||
|
categoryList = append(categoryList, strings.TrimSpace(atomCategory.Label))
|
||||||
|
} else {
|
||||||
|
categoryList = append(categoryList, strings.TrimSpace(atomCategory.Term))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return categoryList
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://tools.ietf.org/html/rfc4685#section-4
|
||||||
|
// If the type attribute of the atom:link is omitted, its value is assumed to be "application/atom+xml".
|
||||||
|
// We accept only HTML or XHTML documents for now since the intention is to have the same behavior as RSS.
|
||||||
|
func (a *atom10Entry) entryCommentsURL() string {
|
||||||
|
commentsURL := a.Links.firstLinkWithRelationAndType("replies", "text/html", "application/xhtml+xml")
|
||||||
|
if urllib.IsAbsoluteURL(commentsURL) {
|
||||||
|
return commentsURL
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type atom10Text struct {
|
||||||
Type string `xml:"type,attr"`
|
Type string `xml:"type,attr"`
|
||||||
CharData string `xml:",chardata"`
|
CharData string `xml:",chardata"`
|
||||||
InnerXML string `xml:",innerxml"`
|
InnerXML string `xml:",innerxml"`
|
||||||
XHTMLRootElement AtomXHTMLRootElement `xml:"http://www.w3.org/1999/xhtml div"`
|
XHTMLRootElement atomXHTMLRootElement `xml:"http://www.w3.org/1999/xhtml div"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Atom10Text) Body() string {
|
type atom10Category struct {
|
||||||
var content string
|
Term string `xml:"term,attr"`
|
||||||
|
Label string `xml:"label,attr"`
|
||||||
if strings.EqualFold(a.Type, "xhtml") {
|
|
||||||
content = a.xhtmlContent()
|
|
||||||
} else {
|
|
||||||
content = a.CharData
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.TrimSpace(content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Atom10Text) Title() string {
|
// Text: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.1
|
||||||
|
// HTML: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.2
|
||||||
|
// XHTML: https://datatracker.ietf.org/doc/html/rfc4287#section-3.1.1.3
|
||||||
|
func (a *atom10Text) String() string {
|
||||||
var content string
|
var content string
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.EqualFold(a.Type, "xhtml"):
|
case a.Type == "", a.Type == "text", a.Type == "text/plain":
|
||||||
content = a.xhtmlContent()
|
if strings.HasPrefix(strings.TrimSpace(a.InnerXML), `<![CDATA[`) {
|
||||||
case strings.Contains(a.InnerXML, "<![CDATA["):
|
content = html.EscapeString(a.CharData)
|
||||||
content = html.UnescapeString(a.CharData)
|
} else {
|
||||||
|
content = a.InnerXML
|
||||||
|
}
|
||||||
|
case a.Type == "xhtml":
|
||||||
|
var root = a.XHTMLRootElement
|
||||||
|
if root.XMLName.Local == "div" {
|
||||||
|
content = root.InnerXML
|
||||||
|
} else {
|
||||||
|
content = a.InnerXML
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
content = a.CharData
|
content = a.CharData
|
||||||
}
|
}
|
||||||
|
|
||||||
content = sanitizer.StripTags(content)
|
|
||||||
return strings.TrimSpace(content)
|
return strings.TrimSpace(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Atom10Text) xhtmlContent() string {
|
type atomXHTMLRootElement struct {
|
||||||
if a.XHTMLRootElement.XMLName.Local == "div" {
|
|
||||||
return a.XHTMLRootElement.InnerXML
|
|
||||||
}
|
|
||||||
return a.InnerXML
|
|
||||||
}
|
|
||||||
|
|
||||||
type AtomXHTMLRootElement struct {
|
|
||||||
XMLName xml.Name `xml:"div"`
|
XMLName xml.Name `xml:"div"`
|
||||||
InnerXML string `xml:",innerxml"`
|
InnerXML string `xml:",innerxml"`
|
||||||
}
|
}
|
||||||
|
@ -1,254 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package atom // import "miniflux.app/v2/internal/reader/atom"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"slices"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"miniflux.app/v2/internal/crypto"
|
|
||||||
"miniflux.app/v2/internal/model"
|
|
||||||
"miniflux.app/v2/internal/reader/date"
|
|
||||||
"miniflux.app/v2/internal/reader/sanitizer"
|
|
||||||
"miniflux.app/v2/internal/urllib"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Atom10Adapter struct {
|
|
||||||
atomFeed *Atom10Feed
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAtom10Adapter(atomFeed *Atom10Feed) *Atom10Adapter {
|
|
||||||
return &Atom10Adapter{atomFeed}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Atom10Adapter) BuildFeed(baseURL string) *model.Feed {
|
|
||||||
feed := new(model.Feed)
|
|
||||||
|
|
||||||
// Populate the feed URL.
|
|
||||||
feedURL := a.atomFeed.Links.firstLinkWithRelation("self")
|
|
||||||
if feedURL != "" {
|
|
||||||
if absoluteFeedURL, err := urllib.AbsoluteURL(baseURL, feedURL); err == nil {
|
|
||||||
feed.FeedURL = absoluteFeedURL
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
feed.FeedURL = baseURL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the site URL.
|
|
||||||
siteURL := a.atomFeed.Links.OriginalLink()
|
|
||||||
if siteURL != "" {
|
|
||||||
if absoluteSiteURL, err := urllib.AbsoluteURL(baseURL, siteURL); err == nil {
|
|
||||||
feed.SiteURL = absoluteSiteURL
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
feed.SiteURL = baseURL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the feed title.
|
|
||||||
feed.Title = a.atomFeed.Title.Body()
|
|
||||||
if feed.Title == "" {
|
|
||||||
feed.Title = feed.SiteURL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the feed icon.
|
|
||||||
if a.atomFeed.Icon != "" {
|
|
||||||
if absoluteIconURL, err := urllib.AbsoluteURL(feed.SiteURL, a.atomFeed.Icon); err == nil {
|
|
||||||
feed.IconURL = absoluteIconURL
|
|
||||||
}
|
|
||||||
} else if a.atomFeed.Logo != "" {
|
|
||||||
if absoluteLogoURL, err := urllib.AbsoluteURL(feed.SiteURL, a.atomFeed.Logo); err == nil {
|
|
||||||
feed.IconURL = absoluteLogoURL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
feed.Entries = a.populateEntries(feed.SiteURL)
|
|
||||||
return feed
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Atom10Adapter) populateEntries(siteURL string) model.Entries {
|
|
||||||
entries := make(model.Entries, 0, len(a.atomFeed.Entries))
|
|
||||||
|
|
||||||
for _, atomEntry := range a.atomFeed.Entries {
|
|
||||||
entry := model.NewEntry()
|
|
||||||
|
|
||||||
// Populate the entry URL.
|
|
||||||
entry.URL = atomEntry.Links.OriginalLink()
|
|
||||||
if entry.URL != "" {
|
|
||||||
if absoluteEntryURL, err := urllib.AbsoluteURL(siteURL, entry.URL); err == nil {
|
|
||||||
entry.URL = absoluteEntryURL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the entry content.
|
|
||||||
entry.Content = atomEntry.Content.Body()
|
|
||||||
if entry.Content == "" {
|
|
||||||
entry.Content = atomEntry.Summary.Body()
|
|
||||||
if entry.Content == "" {
|
|
||||||
entry.Content = atomEntry.FirstMediaDescription()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the entry title.
|
|
||||||
entry.Title = atomEntry.Title.Title()
|
|
||||||
if entry.Title == "" {
|
|
||||||
entry.Title = sanitizer.TruncateHTML(entry.Content, 100)
|
|
||||||
if entry.Title == "" {
|
|
||||||
entry.Title = entry.URL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the entry author.
|
|
||||||
authors := atomEntry.Authors.PersonNames()
|
|
||||||
if len(authors) == 0 {
|
|
||||||
authors = a.atomFeed.Authors.PersonNames()
|
|
||||||
}
|
|
||||||
sort.Strings(authors)
|
|
||||||
authors = slices.Compact(authors)
|
|
||||||
entry.Author = strings.Join(authors, ", ")
|
|
||||||
|
|
||||||
// Populate the entry date.
|
|
||||||
for _, value := range []string{atomEntry.Published, atomEntry.Updated} {
|
|
||||||
if value != "" {
|
|
||||||
if parsedDate, err := date.Parse(value); err != nil {
|
|
||||||
slog.Debug("Unable to parse date from Atom 1.0 feed",
|
|
||||||
slog.String("date", value),
|
|
||||||
slog.String("url", entry.URL),
|
|
||||||
slog.Any("error", err),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
entry.Date = parsedDate
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if entry.Date.IsZero() {
|
|
||||||
entry.Date = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate categories.
|
|
||||||
categories := atomEntry.Categories.CategoryNames()
|
|
||||||
if len(categories) == 0 {
|
|
||||||
categories = a.atomFeed.Categories.CategoryNames()
|
|
||||||
}
|
|
||||||
sort.Strings(categories)
|
|
||||||
entry.Tags = slices.Compact(categories)
|
|
||||||
|
|
||||||
// Populate the commentsURL if defined.
|
|
||||||
// See https://tools.ietf.org/html/rfc4685#section-4
|
|
||||||
// If the type attribute of the atom:link is omitted, its value is assumed to be "application/atom+xml".
|
|
||||||
// We accept only HTML or XHTML documents for now since the intention is to have the same behavior as RSS.
|
|
||||||
commentsURL := atomEntry.Links.firstLinkWithRelationAndType("replies", "text/html", "application/xhtml+xml")
|
|
||||||
if urllib.IsAbsoluteURL(commentsURL) {
|
|
||||||
entry.CommentsURL = commentsURL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the entry hash.
|
|
||||||
for _, value := range []string{atomEntry.ID, atomEntry.Links.OriginalLink()} {
|
|
||||||
if value != "" {
|
|
||||||
entry.Hash = crypto.Hash(value)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the entry enclosures.
|
|
||||||
uniqueEnclosuresMap := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, mediaThumbnail := range atomEntry.AllMediaThumbnails() {
|
|
||||||
mediaURL := strings.TrimSpace(mediaThumbnail.URL)
|
|
||||||
if mediaURL == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, found := uniqueEnclosuresMap[mediaURL]; !found {
|
|
||||||
if mediaAbsoluteURL, err := urllib.AbsoluteURL(siteURL, mediaURL); err != nil {
|
|
||||||
slog.Debug("Unable to build absolute URL for media thumbnail",
|
|
||||||
slog.String("url", mediaThumbnail.URL),
|
|
||||||
slog.String("site_url", siteURL),
|
|
||||||
slog.Any("error", err),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
uniqueEnclosuresMap[mediaAbsoluteURL] = true
|
|
||||||
entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
|
|
||||||
URL: mediaAbsoluteURL,
|
|
||||||
MimeType: mediaThumbnail.MimeType(),
|
|
||||||
Size: mediaThumbnail.Size(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, link := range atomEntry.Links.findAllLinksWithRelation("enclosure") {
|
|
||||||
absoluteEnclosureURL, err := urllib.AbsoluteURL(siteURL, link.Href)
|
|
||||||
if err != nil {
|
|
||||||
slog.Debug("Unable to resolve absolute URL for enclosure",
|
|
||||||
slog.String("enclosure_url", link.Href),
|
|
||||||
slog.String("entry_url", entry.URL),
|
|
||||||
slog.Any("error", err),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if _, found := uniqueEnclosuresMap[absoluteEnclosureURL]; !found {
|
|
||||||
uniqueEnclosuresMap[absoluteEnclosureURL] = true
|
|
||||||
length, _ := strconv.ParseInt(link.Length, 10, 0)
|
|
||||||
entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
|
|
||||||
URL: absoluteEnclosureURL,
|
|
||||||
MimeType: link.Type,
|
|
||||||
Size: length,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mediaContent := range atomEntry.AllMediaContents() {
|
|
||||||
mediaURL := strings.TrimSpace(mediaContent.URL)
|
|
||||||
if mediaURL == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if mediaAbsoluteURL, err := urllib.AbsoluteURL(siteURL, mediaURL); err != nil {
|
|
||||||
slog.Debug("Unable to build absolute URL for media content",
|
|
||||||
slog.String("url", mediaContent.URL),
|
|
||||||
slog.String("site_url", siteURL),
|
|
||||||
slog.Any("error", err),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if _, found := uniqueEnclosuresMap[mediaAbsoluteURL]; !found {
|
|
||||||
uniqueEnclosuresMap[mediaAbsoluteURL] = true
|
|
||||||
entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
|
|
||||||
URL: mediaAbsoluteURL,
|
|
||||||
MimeType: mediaContent.MimeType(),
|
|
||||||
Size: mediaContent.Size(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mediaPeerLink := range atomEntry.AllMediaPeerLinks() {
|
|
||||||
mediaURL := strings.TrimSpace(mediaPeerLink.URL)
|
|
||||||
if mediaURL == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if mediaAbsoluteURL, err := urllib.AbsoluteURL(siteURL, mediaURL); err != nil {
|
|
||||||
slog.Debug("Unable to build absolute URL for media peer link",
|
|
||||||
slog.String("url", mediaPeerLink.URL),
|
|
||||||
slog.String("site_url", siteURL),
|
|
||||||
slog.Any("error", err),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if _, found := uniqueEnclosuresMap[mediaAbsoluteURL]; !found {
|
|
||||||
uniqueEnclosuresMap[mediaAbsoluteURL] = true
|
|
||||||
entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
|
|
||||||
URL: mediaAbsoluteURL,
|
|
||||||
MimeType: mediaPeerLink.MimeType(),
|
|
||||||
Size: mediaPeerLink.Size(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entries = append(entries, entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -3,91 +3,77 @@
|
|||||||
|
|
||||||
package atom // import "miniflux.app/v2/internal/reader/atom"
|
package atom // import "miniflux.app/v2/internal/reader/atom"
|
||||||
|
|
||||||
import (
|
import "strings"
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Specs: https://datatracker.ietf.org/doc/html/rfc4287#section-3.2
|
type atomPerson struct {
|
||||||
type AtomPerson struct {
|
Name string `xml:"name"`
|
||||||
// The "atom:name" element's content conveys a human-readable name for the author.
|
|
||||||
// It MAY be the name of a corporation or other entity no individual authors can be named.
|
|
||||||
// Person constructs MUST contain exactly one "atom:name" element, whose content MUST be a string.
|
|
||||||
Name string `xml:"name"`
|
|
||||||
|
|
||||||
// The "atom:email" element's content conveys an e-mail address associated with the Person construct.
|
|
||||||
// Person constructs MAY contain an atom:email element, but MUST NOT contain more than one.
|
|
||||||
// Its content MUST be an e-mail address [RFC2822].
|
|
||||||
// Ordering of the element children of Person constructs MUST NOT be considered significant.
|
|
||||||
Email string `xml:"email"`
|
Email string `xml:"email"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AtomPerson) PersonName() string {
|
func (a *atomPerson) String() string {
|
||||||
name := strings.TrimSpace(a.Name)
|
name := ""
|
||||||
if name != "" {
|
|
||||||
return name
|
switch {
|
||||||
|
case a.Name != "":
|
||||||
|
name = a.Name
|
||||||
|
case a.Email != "":
|
||||||
|
name = a.Email
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.TrimSpace(a.Email)
|
return strings.TrimSpace(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AtomPersons []*AtomPerson
|
type atomAuthors []*atomPerson
|
||||||
|
|
||||||
func (a AtomPersons) PersonNames() []string {
|
func (a atomAuthors) String() string {
|
||||||
var names []string
|
var authors []string
|
||||||
authorNamesMap := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, person := range a {
|
for _, person := range a {
|
||||||
personName := person.PersonName()
|
authors = append(authors, person.String())
|
||||||
if _, ok := authorNamesMap[personName]; !ok {
|
|
||||||
names = append(names, personName)
|
|
||||||
authorNamesMap[personName] = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return names
|
return strings.Join(authors, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specs: https://datatracker.ietf.org/doc/html/rfc4287#section-4.2.7
|
type atomLink struct {
|
||||||
type AtomLink struct {
|
URL string `xml:"href,attr"`
|
||||||
Href string `xml:"href,attr"`
|
|
||||||
Type string `xml:"type,attr"`
|
Type string `xml:"type,attr"`
|
||||||
Rel string `xml:"rel,attr"`
|
Rel string `xml:"rel,attr"`
|
||||||
Length string `xml:"length,attr"`
|
Length string `xml:"length,attr"`
|
||||||
Title string `xml:"title,attr"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AtomLinks []*AtomLink
|
type atomLinks []*atomLink
|
||||||
|
|
||||||
func (a AtomLinks) OriginalLink() string {
|
func (a atomLinks) originalLink() string {
|
||||||
for _, link := range a {
|
for _, link := range a {
|
||||||
if strings.EqualFold(link.Rel, "alternate") {
|
if strings.EqualFold(link.Rel, "alternate") {
|
||||||
return strings.TrimSpace(link.Href)
|
return strings.TrimSpace(link.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
if link.Rel == "" && (link.Type == "" || link.Type == "text/html") {
|
if link.Rel == "" && (link.Type == "" || link.Type == "text/html") {
|
||||||
return strings.TrimSpace(link.Href)
|
return strings.TrimSpace(link.URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AtomLinks) firstLinkWithRelation(relation string) string {
|
func (a atomLinks) firstLinkWithRelation(relation string) string {
|
||||||
for _, link := range a {
|
for _, link := range a {
|
||||||
if strings.EqualFold(link.Rel, relation) {
|
if strings.EqualFold(link.Rel, relation) {
|
||||||
return strings.TrimSpace(link.Href)
|
return strings.TrimSpace(link.URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AtomLinks) firstLinkWithRelationAndType(relation string, contentTypes ...string) string {
|
func (a atomLinks) firstLinkWithRelationAndType(relation string, contentTypes ...string) string {
|
||||||
for _, link := range a {
|
for _, link := range a {
|
||||||
if strings.EqualFold(link.Rel, relation) {
|
if strings.EqualFold(link.Rel, relation) {
|
||||||
for _, contentType := range contentTypes {
|
for _, contentType := range contentTypes {
|
||||||
if strings.EqualFold(link.Type, contentType) {
|
if strings.EqualFold(link.Type, contentType) {
|
||||||
return strings.TrimSpace(link.Href)
|
return strings.TrimSpace(link.URL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,61 +81,3 @@ func (a AtomLinks) firstLinkWithRelationAndType(relation string, contentTypes ..
|
|||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AtomLinks) findAllLinksWithRelation(relation string) []*AtomLink {
|
|
||||||
var links []*AtomLink
|
|
||||||
|
|
||||||
for _, link := range a {
|
|
||||||
if strings.EqualFold(link.Rel, relation) {
|
|
||||||
link.Href = strings.TrimSpace(link.Href)
|
|
||||||
if link.Href != "" {
|
|
||||||
links = append(links, link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return links
|
|
||||||
}
|
|
||||||
|
|
||||||
// The "atom:category" element conveys information about a category
|
|
||||||
// associated with an entry or feed. This specification assigns no
|
|
||||||
// meaning to the content (if any) of this element.
|
|
||||||
//
|
|
||||||
// Specs: https://datatracker.ietf.org/doc/html/rfc4287#section-4.2.2
|
|
||||||
type AtomCategory struct {
|
|
||||||
// The "term" attribute is a string that identifies the category to
|
|
||||||
// which the entry or feed belongs. Category elements MUST have a
|
|
||||||
// "term" attribute.
|
|
||||||
Term string `xml:"term,attr"`
|
|
||||||
|
|
||||||
// The "scheme" attribute is an IRI that identifies a categorization
|
|
||||||
// scheme. Category elements MAY have a "scheme" attribute.
|
|
||||||
Scheme string `xml:"scheme,attr"`
|
|
||||||
|
|
||||||
// The "label" attribute provides a human-readable label for display in
|
|
||||||
// end-user applications. The content of the "label" attribute is
|
|
||||||
// Language-Sensitive. Entities such as "&" and "<" represent
|
|
||||||
// their corresponding characters ("&" and "<", respectively), not
|
|
||||||
// markup. Category elements MAY have a "label" attribute.
|
|
||||||
Label string `xml:"label,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AtomCategories []AtomCategory
|
|
||||||
|
|
||||||
func (ac AtomCategories) CategoryNames() []string {
|
|
||||||
var categories []string
|
|
||||||
|
|
||||||
for _, category := range ac {
|
|
||||||
label := strings.TrimSpace(category.Label)
|
|
||||||
if label != "" {
|
|
||||||
categories = append(categories, label)
|
|
||||||
} else {
|
|
||||||
term := strings.TrimSpace(category.Term)
|
|
||||||
if term != "" {
|
|
||||||
categories = append(categories, term)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return categories
|
|
||||||
}
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
package atom // import "miniflux.app/v2/internal/reader/atom"
|
package atom // import "miniflux.app/v2/internal/reader/atom"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
@ -11,20 +12,45 @@ import (
|
|||||||
xml_decoder "miniflux.app/v2/internal/reader/xml"
|
xml_decoder "miniflux.app/v2/internal/reader/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parse returns a normalized feed struct from a Atom feed.
|
type atomFeed interface {
|
||||||
func Parse(baseURL string, r io.ReadSeeker, version string) (*model.Feed, error) {
|
Transform(baseURL string) *model.Feed
|
||||||
switch version {
|
}
|
||||||
case "0.3":
|
|
||||||
atomFeed := new(Atom03Feed)
|
// Parse returns a normalized feed struct from a Atom feed.
|
||||||
if err := xml_decoder.NewXMLDecoder(r).Decode(atomFeed); err != nil {
|
func Parse(baseURL string, r io.ReadSeeker) (*model.Feed, error) {
|
||||||
return nil, fmt.Errorf("atom: unable to parse Atom 0.3 feed: %w", err)
|
var rawFeed atomFeed
|
||||||
}
|
if getAtomFeedVersion(r) == "0.3" {
|
||||||
return NewAtom03Adapter(atomFeed).BuildFeed(baseURL), nil
|
rawFeed = new(atom03Feed)
|
||||||
default:
|
} else {
|
||||||
atomFeed := new(Atom10Feed)
|
rawFeed = new(atom10Feed)
|
||||||
if err := xml_decoder.NewXMLDecoder(r).Decode(atomFeed); err != nil {
|
}
|
||||||
return nil, fmt.Errorf("atom: unable to parse Atom 1.0 feed: %w", err)
|
r.Seek(0, io.SeekStart)
|
||||||
}
|
|
||||||
return NewAtom10Adapter(atomFeed).BuildFeed(baseURL), nil
|
if err := xml_decoder.NewXMLDecoder(r).Decode(rawFeed); err != nil {
|
||||||
}
|
return nil, fmt.Errorf("atom: unable to parse feed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawFeed.Transform(baseURL), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAtomFeedVersion(data io.ReadSeeker) string {
|
||||||
|
decoder := xml_decoder.NewXMLDecoder(data)
|
||||||
|
for {
|
||||||
|
token, _ := decoder.Token()
|
||||||
|
if token == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if element, ok := token.(xml.StartElement); ok {
|
||||||
|
if element.Name.Local == "feed" {
|
||||||
|
for _, attr := range element.Attr {
|
||||||
|
if attr.Name.Local == "version" && attr.Value == "0.3" {
|
||||||
|
return "0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "1.0"
|
||||||
}
|
}
|
||||||
|
61
internal/reader/atom/parser_test.go
Normal file
61
internal/reader/atom/parser_test.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package atom // import "miniflux.app/v2/internal/reader/atom"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDetectAtom10(t *testing.T) {
|
||||||
|
data := `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
|
||||||
|
<title>Example Feed</title>
|
||||||
|
<link href="http://example.org/"/>
|
||||||
|
<updated>2003-12-13T18:30:02Z</updated>
|
||||||
|
<author>
|
||||||
|
<name>John Doe</name>
|
||||||
|
</author>
|
||||||
|
<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
|
||||||
|
|
||||||
|
<entry>
|
||||||
|
<title>Atom-Powered Robots Run Amok</title>
|
||||||
|
<link href="http://example.org/2003/12/13/atom03"/>
|
||||||
|
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
|
||||||
|
<updated>2003-12-13T18:30:02Z</updated>
|
||||||
|
<summary>Some text.</summary>
|
||||||
|
</entry>
|
||||||
|
|
||||||
|
</feed>`
|
||||||
|
|
||||||
|
version := getAtomFeedVersion(bytes.NewReader([]byte(data)))
|
||||||
|
if version != "1.0" {
|
||||||
|
t.Errorf(`Invalid Atom version detected: %s`, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectAtom03(t *testing.T) {
|
||||||
|
data := `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed version="0.3" xmlns="http://purl.org/atom/ns#">
|
||||||
|
<title>dive into mark</title>
|
||||||
|
<link rel="alternate" type="text/html" href="http://diveintomark.org/"/>
|
||||||
|
<modified>2003-12-13T18:30:02Z</modified>
|
||||||
|
<author><name>Mark Pilgrim</name></author>
|
||||||
|
<entry>
|
||||||
|
<title>Atom 0.3 snapshot</title>
|
||||||
|
<link rel="alternate" type="text/html" href="http://diveintomark.org/2003/12/13/atom03"/>
|
||||||
|
<id>tag:diveintomark.org,2003:3.2397</id>
|
||||||
|
<issued>2003-12-13T08:29:29-04:00</issued>
|
||||||
|
<modified>2003-12-13T18:30:02Z</modified>
|
||||||
|
<summary type="text/plain">This is a test</summary>
|
||||||
|
<content type="text/html" mode="escaped"><![CDATA[<p>HTML content</p>]]></content>
|
||||||
|
</entry>
|
||||||
|
</feed>`
|
||||||
|
|
||||||
|
version := getAtomFeedVersion(bytes.NewReader([]byte(data)))
|
||||||
|
if version != "0.3" {
|
||||||
|
t.Errorf(`Invalid Atom version detected: %s`, version)
|
||||||
|
}
|
||||||
|
}
|
@ -3,13 +3,29 @@
|
|||||||
|
|
||||||
package dublincore // import "miniflux.app/v2/internal/reader/dublincore"
|
package dublincore // import "miniflux.app/v2/internal/reader/dublincore"
|
||||||
|
|
||||||
type DublinCoreChannelElement struct {
|
import (
|
||||||
DublinCoreCreator string `xml:"http://purl.org/dc/elements/1.1/ creator"`
|
"strings"
|
||||||
|
|
||||||
|
"miniflux.app/v2/internal/reader/sanitizer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DublinCoreFeedElement represents Dublin Core feed XML elements.
|
||||||
|
type DublinCoreFeedElement struct {
|
||||||
|
DublinCoreCreator string `xml:"http://purl.org/dc/elements/1.1/ channel>creator"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (feed *DublinCoreFeedElement) GetSanitizedCreator() string {
|
||||||
|
return strings.TrimSpace(sanitizer.StripTags(feed.DublinCoreCreator))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DublinCoreItemElement represents Dublin Core entry XML elements.
|
||||||
type DublinCoreItemElement struct {
|
type DublinCoreItemElement struct {
|
||||||
DublinCoreTitle string `xml:"http://purl.org/dc/elements/1.1/ title"`
|
DublinCoreTitle string `xml:"http://purl.org/dc/elements/1.1/ title"`
|
||||||
DublinCoreDate string `xml:"http://purl.org/dc/elements/1.1/ date"`
|
DublinCoreDate string `xml:"http://purl.org/dc/elements/1.1/ date"`
|
||||||
DublinCoreCreator string `xml:"http://purl.org/dc/elements/1.1/ creator"`
|
DublinCoreCreator string `xml:"http://purl.org/dc/elements/1.1/ creator"`
|
||||||
DublinCoreContent string `xml:"http://purl.org/rss/1.0/modules/content/ encoded"`
|
DublinCoreContent string `xml:"http://purl.org/rss/1.0/modules/content/ encoded"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (item *DublinCoreItemElement) GetSanitizedCreator() string {
|
||||||
|
return strings.TrimSpace(sanitizer.StripTags(item.DublinCoreCreator))
|
||||||
|
}
|
||||||
|
@ -35,3 +35,8 @@ func CharsetReader(charsetLabel string, input io.Reader) (io.Reader, error) {
|
|||||||
// Transform document to UTF-8 from the specified encoding in XML prolog.
|
// Transform document to UTF-8 from the specified encoding in XML prolog.
|
||||||
return charset.NewReaderLabel(charsetLabel, r)
|
return charset.NewReaderLabel(charsetLabel, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CharsetReaderFromContentType is used when the encoding is not specified for the input document.
|
||||||
|
func CharsetReaderFromContentType(contentType string, input io.Reader) (io.Reader, error) {
|
||||||
|
return charset.NewReader(input, contentType)
|
||||||
|
}
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
package fetcher
|
|
||||||
|
|
||||||
import (
|
|
||||||
"compress/gzip"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/andybalholm/brotli"
|
|
||||||
)
|
|
||||||
|
|
||||||
type brotliReadCloser struct {
|
|
||||||
body io.ReadCloser
|
|
||||||
brotliReader io.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBrotliReadCloser(body io.ReadCloser) *brotliReadCloser {
|
|
||||||
return &brotliReadCloser{
|
|
||||||
body: body,
|
|
||||||
brotliReader: brotli.NewReader(body),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *brotliReadCloser) Read(p []byte) (n int, err error) {
|
|
||||||
return b.brotliReader.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *brotliReadCloser) Close() error {
|
|
||||||
return b.body.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
type gzipReadCloser struct {
|
|
||||||
body io.ReadCloser
|
|
||||||
gzipReader io.Reader
|
|
||||||
gzipErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGzipReadCloser(body io.ReadCloser) *gzipReadCloser {
|
|
||||||
return &gzipReadCloser{body: body}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gz *gzipReadCloser) Read(p []byte) (n int, err error) {
|
|
||||||
if gz.gzipReader == nil {
|
|
||||||
if gz.gzipErr == nil {
|
|
||||||
gz.gzipReader, gz.gzipErr = gzip.NewReader(gz.body)
|
|
||||||
}
|
|
||||||
if gz.gzipErr != nil {
|
|
||||||
return 0, gz.gzipErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gz.gzipReader.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gz *gzipReadCloser) Close() error {
|
|
||||||
return gz.body.Close()
|
|
||||||
}
|
|
@ -109,16 +109,6 @@ func (r *RequestBuilder) IgnoreTLSErrors(value bool) *RequestBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RequestBuilder) ExecuteRequest(requestURL string) (*http.Response, error) {
|
func (r *RequestBuilder) ExecuteRequest(requestURL string) (*http.Response, error) {
|
||||||
// We get the safe ciphers
|
|
||||||
ciphers := tls.CipherSuites()
|
|
||||||
if r.ignoreTLSErrors {
|
|
||||||
// and the insecure ones if we are ignoring TLS errors. This allows to connect to badly configured servers anyway
|
|
||||||
ciphers = append(ciphers, tls.InsecureCipherSuites()...)
|
|
||||||
}
|
|
||||||
cipherSuites := []uint16{}
|
|
||||||
for _, cipher := range ciphers {
|
|
||||||
cipherSuites = append(cipherSuites, cipher.ID)
|
|
||||||
}
|
|
||||||
transport := &http.Transport{
|
transport := &http.Transport{
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
// Setting `DialContext` disables HTTP/2, this option forces the transport to try HTTP/2 regardless.
|
// Setting `DialContext` disables HTTP/2, this option forces the transport to try HTTP/2 regardless.
|
||||||
@ -138,7 +128,6 @@ func (r *RequestBuilder) ExecuteRequest(requestURL string) (*http.Response, erro
|
|||||||
IdleConnTimeout: 10 * time.Second,
|
IdleConnTimeout: 10 * time.Second,
|
||||||
|
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
CipherSuites: cipherSuites,
|
|
||||||
InsecureSkipVerify: r.ignoreTLSErrors,
|
InsecureSkipVerify: r.ignoreTLSErrors,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -180,7 +169,6 @@ func (r *RequestBuilder) ExecuteRequest(requestURL string) (*http.Response, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.Header = r.headers
|
req.Header = r.headers
|
||||||
req.Header.Set("Accept-Encoding", "br, gzip")
|
|
||||||
req.Header.Set("Accept", defaultAcceptHeader)
|
req.Header.Set("Accept", defaultAcceptHeader)
|
||||||
req.Header.Set("Connection", "close")
|
req.Header.Set("Connection", "close")
|
||||||
|
|
||||||
|
@ -8,12 +8,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"miniflux.app/v2/internal/locale"
|
"miniflux.app/v2/internal/locale"
|
||||||
)
|
)
|
||||||
@ -56,12 +52,12 @@ func (r *ResponseHandler) IsModified(lastEtagValue, lastModifiedValue string) bo
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.ETag() != "" {
|
if r.ETag() != "" && r.ETag() == lastEtagValue {
|
||||||
return r.ETag() != lastEtagValue
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.LastModified() != "" {
|
if r.LastModified() != "" && r.LastModified() == lastModifiedValue {
|
||||||
return r.LastModified() != lastModifiedValue
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@ -73,31 +69,12 @@ func (r *ResponseHandler) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ResponseHandler) getReader(maxBodySize int64) io.ReadCloser {
|
|
||||||
contentEncoding := strings.ToLower(r.httpResponse.Header.Get("Content-Encoding"))
|
|
||||||
slog.Debug("Request response",
|
|
||||||
slog.String("effective_url", r.EffectiveURL()),
|
|
||||||
slog.String("content_length", r.httpResponse.Header.Get("Content-Length")),
|
|
||||||
slog.String("content_encoding", contentEncoding),
|
|
||||||
slog.String("content_type", r.httpResponse.Header.Get("Content-Type")),
|
|
||||||
)
|
|
||||||
|
|
||||||
reader := r.httpResponse.Body
|
|
||||||
switch contentEncoding {
|
|
||||||
case "br":
|
|
||||||
reader = NewBrotliReadCloser(r.httpResponse.Body)
|
|
||||||
case "gzip":
|
|
||||||
reader = NewGzipReadCloser(r.httpResponse.Body)
|
|
||||||
}
|
|
||||||
return http.MaxBytesReader(nil, reader, maxBodySize)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ResponseHandler) Body(maxBodySize int64) io.ReadCloser {
|
func (r *ResponseHandler) Body(maxBodySize int64) io.ReadCloser {
|
||||||
return r.getReader(maxBodySize)
|
return http.MaxBytesReader(nil, r.httpResponse.Body, maxBodySize)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ResponseHandler) ReadBody(maxBodySize int64) ([]byte, *locale.LocalizedErrorWrapper) {
|
func (r *ResponseHandler) ReadBody(maxBodySize int64) ([]byte, *locale.LocalizedErrorWrapper) {
|
||||||
limitedReader := r.getReader(maxBodySize)
|
limitedReader := http.MaxBytesReader(nil, r.httpResponse.Body, maxBodySize)
|
||||||
|
|
||||||
buffer, err := io.ReadAll(limitedReader)
|
buffer, err := io.ReadAll(limitedReader)
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
@ -117,18 +94,23 @@ func (r *ResponseHandler) ReadBody(maxBodySize int64) ([]byte, *locale.Localized
|
|||||||
|
|
||||||
func (r *ResponseHandler) LocalizedError() *locale.LocalizedErrorWrapper {
|
func (r *ResponseHandler) LocalizedError() *locale.LocalizedErrorWrapper {
|
||||||
if r.clientErr != nil {
|
if r.clientErr != nil {
|
||||||
switch {
|
switch r.clientErr.(type) {
|
||||||
case isSSLError(r.clientErr):
|
case x509.CertificateInvalidError, x509.HostnameError:
|
||||||
return locale.NewLocalizedErrorWrapper(fmt.Errorf("fetcher: %w", r.clientErr), "error.tls_error", r.clientErr)
|
return locale.NewLocalizedErrorWrapper(fmt.Errorf("fetcher: %w", r.clientErr), "error.tls_error", r.clientErr)
|
||||||
case isNetworkError(r.clientErr):
|
case *net.OpError:
|
||||||
return locale.NewLocalizedErrorWrapper(fmt.Errorf("fetcher: %w", r.clientErr), "error.network_operation", r.clientErr)
|
return locale.NewLocalizedErrorWrapper(fmt.Errorf("fetcher: %w", r.clientErr), "error.network_operation", r.clientErr)
|
||||||
case os.IsTimeout(r.clientErr):
|
case net.Error:
|
||||||
return locale.NewLocalizedErrorWrapper(fmt.Errorf("fetcher: %w", r.clientErr), "error.network_timeout", r.clientErr)
|
networkErr := r.clientErr.(net.Error)
|
||||||
case errors.Is(r.clientErr, io.EOF):
|
if networkErr.Timeout() {
|
||||||
return locale.NewLocalizedErrorWrapper(fmt.Errorf("fetcher: %w", r.clientErr), "error.http_empty_response")
|
return locale.NewLocalizedErrorWrapper(fmt.Errorf("fetcher: %w", r.clientErr), "error.network_timeout", r.clientErr)
|
||||||
default:
|
}
|
||||||
return locale.NewLocalizedErrorWrapper(fmt.Errorf("fetcher: %w", r.clientErr), "error.http_client_error", r.clientErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errors.Is(r.clientErr, io.EOF) {
|
||||||
|
return locale.NewLocalizedErrorWrapper(fmt.Errorf("fetcher: %w", r.clientErr), "error.http_empty_response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return locale.NewLocalizedErrorWrapper(fmt.Errorf("fetcher: %w", r.clientErr), "error.http_client_error", r.clientErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.httpResponse.StatusCode {
|
switch r.httpResponse.StatusCode {
|
||||||
@ -163,32 +145,3 @@ func (r *ResponseHandler) LocalizedError() *locale.LocalizedErrorWrapper {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isNetworkError(err error) bool {
|
|
||||||
if _, ok := err.(*url.Error); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if err == io.EOF {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
var opErr *net.OpError
|
|
||||||
if ok := errors.As(err, &opErr); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSSLError(err error) bool {
|
|
||||||
var certErr x509.UnknownAuthorityError
|
|
||||||
if errors.As(err, &certErr) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostErr x509.HostnameError
|
|
||||||
if errors.As(err, &hostErr) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var algErr x509.InsecureAlgorithmError
|
|
||||||
return errors.As(err, &algErr)
|
|
||||||
}
|
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package fetcher // import "miniflux.app/v2/internal/reader/fetcher"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestIsModified(t *testing.T) {
|
|
||||||
var cachedEtag = "abc123"
|
|
||||||
var cachedLastModified = "Wed, 21 Oct 2015 07:28:00 GMT"
|
|
||||||
|
|
||||||
var testCases = map[string]struct {
|
|
||||||
Status int
|
|
||||||
LastModified string
|
|
||||||
ETag string
|
|
||||||
IsModified bool
|
|
||||||
}{
|
|
||||||
"Unmodified 304": {
|
|
||||||
Status: 304,
|
|
||||||
LastModified: cachedLastModified,
|
|
||||||
ETag: cachedEtag,
|
|
||||||
IsModified: false,
|
|
||||||
},
|
|
||||||
"Unmodified 200": {
|
|
||||||
Status: 200,
|
|
||||||
LastModified: cachedLastModified,
|
|
||||||
ETag: cachedEtag,
|
|
||||||
IsModified: false,
|
|
||||||
},
|
|
||||||
// This case is invalid per RFC9110 8.8.1, so ETag takes precedence.
|
|
||||||
"Last-Modified changed only": {
|
|
||||||
Status: 200,
|
|
||||||
LastModified: "Thu, 22 Oct 2015 07:28:00 GMT",
|
|
||||||
ETag: cachedEtag,
|
|
||||||
IsModified: false,
|
|
||||||
},
|
|
||||||
"ETag changed only": {
|
|
||||||
Status: 200,
|
|
||||||
LastModified: cachedLastModified,
|
|
||||||
ETag: "xyz789",
|
|
||||||
IsModified: true,
|
|
||||||
},
|
|
||||||
"ETag and Last-Modified changed": {
|
|
||||||
Status: 200,
|
|
||||||
LastModified: "Thu, 22 Oct 2015 07:28:00 GMT",
|
|
||||||
ETag: "xyz789",
|
|
||||||
IsModified: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for name, tc := range testCases {
|
|
||||||
t.Run(name, func(tt *testing.T) {
|
|
||||||
header := http.Header{}
|
|
||||||
header.Add("Last-Modified", tc.LastModified)
|
|
||||||
header.Add("ETag", tc.ETag)
|
|
||||||
rh := ResponseHandler{
|
|
||||||
httpResponse: &http.Response{
|
|
||||||
StatusCode: tc.Status,
|
|
||||||
Header: header,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if tc.IsModified != rh.IsModified(cachedEtag, cachedLastModified) {
|
|
||||||
tt.Error(name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package googleplay // import "miniflux.app/v2/internal/reader/googleplay"
|
|
||||||
|
|
||||||
// Specs:
|
|
||||||
// https://support.google.com/googleplay/podcasts/answer/6260341
|
|
||||||
// https://www.google.com/schemas/play-podcasts/1.0/play-podcasts.xsd
|
|
||||||
type GooglePlayChannelElement struct {
|
|
||||||
GooglePlayAuthor string `xml:"http://www.google.com/schemas/play-podcasts/1.0 author"`
|
|
||||||
GooglePlayEmail string `xml:"http://www.google.com/schemas/play-podcasts/1.0 email"`
|
|
||||||
GooglePlayImage GooglePlayImageElement `xml:"http://www.google.com/schemas/play-podcasts/1.0 image"`
|
|
||||||
GooglePlayDescription string `xml:"http://www.google.com/schemas/play-podcasts/1.0 description"`
|
|
||||||
GooglePlayCategory GooglePlayCategoryElement `xml:"http://www.google.com/schemas/play-podcasts/1.0 category"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GooglePlayItemElement struct {
|
|
||||||
GooglePlayAuthor string `xml:"http://www.google.com/schemas/play-podcasts/1.0 author"`
|
|
||||||
GooglePlayDescription string `xml:"http://www.google.com/schemas/play-podcasts/1.0 description"`
|
|
||||||
GooglePlayExplicit string `xml:"http://www.google.com/schemas/play-podcasts/1.0 explicit"`
|
|
||||||
GooglePlayBlock string `xml:"http://www.google.com/schemas/play-podcasts/1.0 block"`
|
|
||||||
GooglePlayNewFeedURL string `xml:"http://www.google.com/schemas/play-podcasts/1.0 new-feed-url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GooglePlayImageElement struct {
|
|
||||||
Href string `xml:"href,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GooglePlayCategoryElement struct {
|
|
||||||
Text string `xml:"text,attr"`
|
|
||||||
}
|
|
@ -169,7 +169,6 @@ func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model
|
|||||||
subscription.BlocklistRules = feedCreationRequest.BlocklistRules
|
subscription.BlocklistRules = feedCreationRequest.BlocklistRules
|
||||||
subscription.KeeplistRules = feedCreationRequest.KeeplistRules
|
subscription.KeeplistRules = feedCreationRequest.KeeplistRules
|
||||||
subscription.UrlRewriteRules = feedCreationRequest.UrlRewriteRules
|
subscription.UrlRewriteRules = feedCreationRequest.UrlRewriteRules
|
||||||
subscription.HideGlobally = feedCreationRequest.HideGlobally
|
|
||||||
subscription.EtagHeader = responseHandler.ETag()
|
subscription.EtagHeader = responseHandler.ETag()
|
||||||
subscription.LastModifiedHeader = responseHandler.LastModified()
|
subscription.LastModifiedHeader = responseHandler.LastModified()
|
||||||
subscription.FeedURL = responseHandler.EffectiveURL()
|
subscription.FeedURL = responseHandler.EffectiveURL()
|
||||||
@ -237,18 +236,14 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
|
|||||||
requestBuilder.WithUsernameAndPassword(originalFeed.Username, originalFeed.Password)
|
requestBuilder.WithUsernameAndPassword(originalFeed.Username, originalFeed.Password)
|
||||||
requestBuilder.WithUserAgent(originalFeed.UserAgent, config.Opts.HTTPClientUserAgent())
|
requestBuilder.WithUserAgent(originalFeed.UserAgent, config.Opts.HTTPClientUserAgent())
|
||||||
requestBuilder.WithCookie(originalFeed.Cookie)
|
requestBuilder.WithCookie(originalFeed.Cookie)
|
||||||
|
requestBuilder.WithETag(originalFeed.EtagHeader)
|
||||||
|
requestBuilder.WithLastModified(originalFeed.LastModifiedHeader)
|
||||||
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
|
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
|
||||||
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
|
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
|
||||||
requestBuilder.UseProxy(originalFeed.FetchViaProxy)
|
requestBuilder.UseProxy(originalFeed.FetchViaProxy)
|
||||||
requestBuilder.IgnoreTLSErrors(originalFeed.AllowSelfSignedCertificates)
|
requestBuilder.IgnoreTLSErrors(originalFeed.AllowSelfSignedCertificates)
|
||||||
requestBuilder.DisableHTTP2(originalFeed.DisableHTTP2)
|
requestBuilder.DisableHTTP2(originalFeed.DisableHTTP2)
|
||||||
|
|
||||||
ignoreHTTPCache := originalFeed.IgnoreHTTPCache || forceRefresh
|
|
||||||
if !ignoreHTTPCache {
|
|
||||||
requestBuilder.WithETag(originalFeed.EtagHeader)
|
|
||||||
requestBuilder.WithLastModified(originalFeed.LastModifiedHeader)
|
|
||||||
}
|
|
||||||
|
|
||||||
responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(originalFeed.FeedURL))
|
responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(originalFeed.FeedURL))
|
||||||
defer responseHandler.Close()
|
defer responseHandler.Close()
|
||||||
|
|
||||||
@ -266,7 +261,7 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
|
|||||||
return localizedError
|
return localizedError
|
||||||
}
|
}
|
||||||
|
|
||||||
if ignoreHTTPCache || responseHandler.IsModified(originalFeed.EtagHeader, originalFeed.LastModifiedHeader) {
|
if originalFeed.IgnoreHTTPCache || responseHandler.IsModified(originalFeed.EtagHeader, originalFeed.LastModifiedHeader) {
|
||||||
slog.Debug("Feed modified",
|
slog.Debug("Feed modified",
|
||||||
slog.Int64("user_id", userID),
|
slog.Int64("user_id", userID),
|
||||||
slog.Int64("feed_id", feedID),
|
slog.Int64("feed_id", feedID),
|
||||||
|
@ -15,11 +15,11 @@ import (
|
|||||||
"miniflux.app/v2/internal/config"
|
"miniflux.app/v2/internal/config"
|
||||||
"miniflux.app/v2/internal/crypto"
|
"miniflux.app/v2/internal/crypto"
|
||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
|
"miniflux.app/v2/internal/reader/encoding"
|
||||||
"miniflux.app/v2/internal/reader/fetcher"
|
"miniflux.app/v2/internal/reader/fetcher"
|
||||||
"miniflux.app/v2/internal/urllib"
|
"miniflux.app/v2/internal/urllib"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
"golang.org/x/net/html/charset"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type IconFinder struct {
|
type IconFinder struct {
|
||||||
@ -191,7 +191,7 @@ func findIconURLsFromHTMLDocument(body io.Reader, contentType string) ([]string,
|
|||||||
"link[rel='apple-touch-icon-precomposed.png']",
|
"link[rel='apple-touch-icon-precomposed.png']",
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlDocumentReader, err := charset.NewReader(body, contentType)
|
htmlDocumentReader, err := encoding.CharsetReaderFromContentType(contentType, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("icon: unable to create charset reader: %w", err)
|
return nil, fmt.Errorf("icon: unable to create charset reader: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package itunes // import "miniflux.app/v2/internal/reader/itunes"
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
// Specs: https://help.apple.com/itc/podcasts_connect/#/itcb54353390
|
|
||||||
type ItunesChannelElement struct {
|
|
||||||
ItunesAuthor string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd author"`
|
|
||||||
ItunesBlock string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd block"`
|
|
||||||
ItunesCategories []ItunesCategoryElement `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd category"`
|
|
||||||
ItunesComplete string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd complete"`
|
|
||||||
ItunesCopyright string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd copyright"`
|
|
||||||
ItunesExplicit string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd explicit"`
|
|
||||||
ItunesImage ItunesImageElement `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd image"`
|
|
||||||
Keywords string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd keywords"`
|
|
||||||
ItunesNewFeedURL string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd new-feed-url"`
|
|
||||||
ItunesOwner ItunesOwnerElement `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd owner"`
|
|
||||||
ItunesSummary string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd summary"`
|
|
||||||
ItunesTitle string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd title"`
|
|
||||||
ItunesType string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *ItunesChannelElement) GetItunesCategories() []string {
|
|
||||||
var categories []string
|
|
||||||
for _, category := range i.ItunesCategories {
|
|
||||||
categories = append(categories, category.Text)
|
|
||||||
if category.SubCategory != nil {
|
|
||||||
categories = append(categories, category.SubCategory.Text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return categories
|
|
||||||
}
|
|
||||||
|
|
||||||
type ItunesItemElement struct {
|
|
||||||
ItunesAuthor string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd author"`
|
|
||||||
ItunesEpisode string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd episode"`
|
|
||||||
ItunesEpisodeType string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd episodeType"`
|
|
||||||
ItunesExplicit string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd explicit"`
|
|
||||||
ItunesDuration string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd duration"`
|
|
||||||
ItunesImage ItunesImageElement `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd image"`
|
|
||||||
ItunesSeason string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd season"`
|
|
||||||
ItunesSubtitle string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd subtitle"`
|
|
||||||
ItunesSummary string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd summary"`
|
|
||||||
ItunesTitle string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd title"`
|
|
||||||
ItunesTranscript string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd transcript"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ItunesImageElement struct {
|
|
||||||
Href string `xml:"href,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ItunesCategoryElement struct {
|
|
||||||
Text string `xml:"text,attr"`
|
|
||||||
SubCategory *ItunesCategoryElement `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd category"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ItunesOwnerElement struct {
|
|
||||||
Name string `xml:"name"`
|
|
||||||
Email string `xml:"email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *ItunesOwnerElement) String() string {
|
|
||||||
var name string
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case i.Name != "":
|
|
||||||
name = i.Name
|
|
||||||
case i.Email != "":
|
|
||||||
name = i.Email
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.TrimSpace(name)
|
|
||||||
}
|
|
@ -1,172 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package json // import "miniflux.app/v2/internal/reader/json"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"miniflux.app/v2/internal/crypto"
|
|
||||||
"miniflux.app/v2/internal/model"
|
|
||||||
"miniflux.app/v2/internal/reader/date"
|
|
||||||
"miniflux.app/v2/internal/reader/sanitizer"
|
|
||||||
"miniflux.app/v2/internal/urllib"
|
|
||||||
)
|
|
||||||
|
|
||||||
type JSONAdapter struct {
|
|
||||||
jsonFeed *JSONFeed
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewJSONAdapter(jsonFeed *JSONFeed) *JSONAdapter {
|
|
||||||
return &JSONAdapter{jsonFeed}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *JSONAdapter) BuildFeed(baseURL string) *model.Feed {
|
|
||||||
feed := &model.Feed{
|
|
||||||
Title: strings.TrimSpace(j.jsonFeed.Title),
|
|
||||||
FeedURL: strings.TrimSpace(j.jsonFeed.FeedURL),
|
|
||||||
SiteURL: strings.TrimSpace(j.jsonFeed.HomePageURL),
|
|
||||||
}
|
|
||||||
|
|
||||||
if feed.FeedURL == "" {
|
|
||||||
feed.FeedURL = strings.TrimSpace(baseURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to the feed URL if the site URL is empty.
|
|
||||||
if feed.SiteURL == "" {
|
|
||||||
feed.SiteURL = feed.FeedURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if feedURL, err := urllib.AbsoluteURL(baseURL, feed.FeedURL); err == nil {
|
|
||||||
feed.FeedURL = feedURL
|
|
||||||
}
|
|
||||||
|
|
||||||
if siteURL, err := urllib.AbsoluteURL(baseURL, feed.SiteURL); err == nil {
|
|
||||||
feed.SiteURL = siteURL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to the feed URL if the title is empty.
|
|
||||||
if feed.Title == "" {
|
|
||||||
feed.Title = feed.SiteURL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the icon URL if present.
|
|
||||||
for _, iconURL := range []string{j.jsonFeed.FaviconURL, j.jsonFeed.IconURL} {
|
|
||||||
iconURL = strings.TrimSpace(iconURL)
|
|
||||||
if iconURL != "" {
|
|
||||||
if absoluteIconURL, err := urllib.AbsoluteURL(feed.SiteURL, iconURL); err == nil {
|
|
||||||
feed.IconURL = absoluteIconURL
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range j.jsonFeed.Items {
|
|
||||||
entry := model.NewEntry()
|
|
||||||
entry.Title = strings.TrimSpace(item.Title)
|
|
||||||
entry.URL = strings.TrimSpace(item.URL)
|
|
||||||
|
|
||||||
// Make sure the entry URL is absolute.
|
|
||||||
if entryURL, err := urllib.AbsoluteURL(feed.SiteURL, entry.URL); err == nil {
|
|
||||||
entry.URL = entryURL
|
|
||||||
}
|
|
||||||
|
|
||||||
// The entry title is optional, so we need to find a fallback.
|
|
||||||
if entry.Title == "" {
|
|
||||||
for _, value := range []string{item.Summary, item.ContentText, item.ContentHTML} {
|
|
||||||
if value != "" {
|
|
||||||
entry.Title = sanitizer.TruncateHTML(value, 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to the entry URL if the title is empty.
|
|
||||||
if entry.Title == "" {
|
|
||||||
entry.Title = entry.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the entry content.
|
|
||||||
for _, value := range []string{item.ContentHTML, item.ContentText, item.Summary} {
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
if value != "" {
|
|
||||||
entry.Content = value
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the entry date.
|
|
||||||
for _, value := range []string{item.DatePublished, item.DateModified} {
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
if value != "" {
|
|
||||||
if date, err := date.Parse(value); err != nil {
|
|
||||||
slog.Debug("Unable to parse date from JSON feed",
|
|
||||||
slog.String("date", value),
|
|
||||||
slog.String("url", entry.URL),
|
|
||||||
slog.Any("error", err),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
entry.Date = date
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if entry.Date.IsZero() {
|
|
||||||
entry.Date = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the entry author.
|
|
||||||
itemAuthors := j.jsonFeed.Authors
|
|
||||||
itemAuthors = append(itemAuthors, item.Authors...)
|
|
||||||
itemAuthors = append(itemAuthors, item.Author, j.jsonFeed.Author)
|
|
||||||
|
|
||||||
var authorNames []string
|
|
||||||
for _, author := range itemAuthors {
|
|
||||||
authorName := strings.TrimSpace(author.Name)
|
|
||||||
if authorName != "" {
|
|
||||||
authorNames = append(authorNames, authorName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
slices.Sort(authorNames)
|
|
||||||
authorNames = slices.Compact(authorNames)
|
|
||||||
entry.Author = strings.Join(authorNames, ", ")
|
|
||||||
|
|
||||||
// Populate the entry enclosures.
|
|
||||||
for _, attachment := range item.Attachments {
|
|
||||||
attachmentURL := strings.TrimSpace(attachment.URL)
|
|
||||||
if attachmentURL != "" {
|
|
||||||
if absoluteAttachmentURL, err := urllib.AbsoluteURL(feed.SiteURL, attachmentURL); err == nil {
|
|
||||||
entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
|
|
||||||
URL: absoluteAttachmentURL,
|
|
||||||
MimeType: attachment.MimeType,
|
|
||||||
Size: attachment.Size,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate the entry tags.
|
|
||||||
for _, tag := range item.Tags {
|
|
||||||
tag = strings.TrimSpace(tag)
|
|
||||||
if tag != "" {
|
|
||||||
entry.Tags = append(entry.Tags, tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a hash for the entry.
|
|
||||||
for _, value := range []string{item.ID, item.URL, item.ContentText + item.ContentHTML + item.Summary} {
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
if value != "" {
|
|
||||||
entry.Hash = crypto.Hash(value)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
feed.Entries = append(feed.Entries, entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
return feed
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user