Compare commits

..

84 Commits

Author SHA1 Message Date
Frédéric Guillot f68046cce0 docs(changelog): update release notes for version 2.2.8 2025-04-22 21:01:43 -07:00
Frédéric Guillot d33e305af9 fix(api): hide_globally categories field should be a boolean 2025-04-21 19:43:25 -07:00
Frédéric Guillot 764212f37c refactor(js): replace DomHelper methods with standalone functions 2025-04-17 18:15:08 -07:00
Tali Auster e02b65d4bc fix: deal with navigator.share exceptions
Navigator.share returns a promise that's executed in the background, but
unless we await it explicitly, we won't get the exceptions in the
try/catch block.
2025-04-17 17:07:38 -07:00
Tali Auster fe7ec25a09 chore: fix indentation 2025-04-17 17:07:38 -07:00
Tali Auster 2959a4d2bf fix: clarify share flow in UI
Prior to this commit, to share an entry, a user has to click on the
share link and then copy the URL they are redirected to. The danger is
that they may right-click and copy the share link without actually
clicking on it, and therefore share a link that, when authenticated,
shares the entry, rather than actually sharing the entry.

Here, we avoid this misinterpretation by making sharing into a POST
request and using a form rather than a link.
2025-04-17 17:07:38 -07:00
AiraNadih 6b70a7dc81 feat(api): add update_content query parameter to /entries/{entryID}/fetch-content endpoint 2025-04-17 12:41:58 -07:00
dependabot[bot] 495c6aacd9 build(deps): bump github.com/mattn/go-sqlite3 from 1.14.27 to 1.14.28
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.27 to 1.14.28.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.27...v1.14.28)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.28
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-16 19:27:11 -07:00
Frédéric Guillot bc228d0afe ci: add documentation issue template 2025-04-12 14:29:52 -07:00
Frédéric Guillot d139d8a6ce feat(cli): add -reset-feed-next-check-at argument 2025-04-11 15:56:57 -07:00
dependabot[bot] 28d0185e79 build(deps): bump github.com/PuerkitoBio/goquery from 1.10.2 to 1.10.3
Bumps [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) from 1.10.2 to 1.10.3.
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.10.2...v1.10.3)

---
updated-dependencies:
- dependency-name: github.com/PuerkitoBio/goquery
  dependency-version: 1.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-11 15:44:47 -07:00
Frédéric Guillot c87c93d85f feat(config): add SCHEDULER_ROUND_ROBIN_MAX_INTERVAL option
Add option to cap maximum refresh interval when RSS TTL, Retry-After, Cache-Control, or Expires headers specify excessively high values.
2025-04-11 15:40:32 -07:00
dependabot[bot] 0ef21e85c2 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.1 to 1.22.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.21.1...v1.22.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 20:53:15 -07:00
dependabot[bot] 7438d67061 build(deps): bump github.com/tdewolff/minify/v2 from 2.22.4 to 2.23.1
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.22.4 to 2.23.1.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.22.4...v2.23.1)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-version: 2.23.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 20:49:12 -07:00
dependabot[bot] e4399b4f9a build(deps): bump golang.org/x/image from 0.25.0 to 0.26.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.25.0 to 0.26.0.
- [Commits](https://github.com/golang/image/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 20:48:54 -07:00
dependabot[bot] b4f891705a build(deps): bump golang.org/x/oauth2 from 0.28.0 to 0.29.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.28.0 to 0.29.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 20:10:31 -07:00
dependabot[bot] 3d72404ace build(deps): bump golang.org/x/net from 0.38.0 to 0.39.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.38.0 to 0.39.0.
- [Commits](https://github.com/golang/net/compare/v0.38.0...v0.39.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 19:55:08 -07:00
Matthaiks c4e92e6111 feat(locale): update Polish translation 2025-04-07 12:06:44 -07:00
Frédéric Guillot ef22e95f8b feat: implement proxy URL per feed 2025-04-06 21:05:19 -07:00
tssujt 7b344de846 feat(telegrambot): replace "Go to website" button with "Go to Miniflux" 2025-04-06 18:30:42 -07:00
Frédéric Guillot c45b51d1f8 feat: use Cache-Control max-age and Expires headers to calculate next check 2025-04-06 16:24:00 -07:00
Frédéric Guillot 0af1a6e121 refactor: avoid logging twice the feed errors in the background worker 2025-04-06 15:39:40 -07:00
Frédéric Guillot 535fd050b7 feat: add proxy rotation functionality 2025-04-06 14:59:00 -07:00
Frédéric Guillot d20e8a4e2c ci(linter): replace commitlint with a Python script 2025-04-05 20:41:34 -07:00
Qeynos 7514e8a0c1 feat(locale): update Chinese translation 2025-04-04 19:40:44 -07:00
Cthulhux ca3ede3183 Update de_DE.json
More translations
2025-04-04 19:37:53 -07:00
dependabot[bot] 038d33600a build(deps): bump github.com/coreos/go-oidc/v3 from 3.13.0 to 3.14.1
Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc) from 3.13.0 to 3.14.1.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v3.13.0...v3.14.1)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-oidc/v3
  dependency-version: 3.14.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-03 17:34:17 -07:00
dependabot[bot] 1a3a174688 build(deps): bump github.com/mattn/go-sqlite3 from 1.14.25 to 1.14.27
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.25 to 1.14.27.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.25...v1.14.27)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-02 19:25:34 -07:00
dependabot[bot] 6848c759b5 build(deps): bump github.com/mattn/go-sqlite3 from 1.14.24 to 1.14.25
Bumps [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) from 1.14.24 to 1.14.25.
- [Release notes](https://github.com/mattn/go-sqlite3/releases)
- [Commits](https://github.com/mattn/go-sqlite3/compare/v1.14.24...v1.14.25)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-sqlite3
  dependency-version: 1.14.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 19:46:53 -07:00
dependabot[bot] 10578f512f build(deps): bump github.com/go-webauthn/webauthn from 0.12.2 to 0.12.3
Bumps [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) from 0.12.2 to 0.12.3.
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.12.2...v0.12.3)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-version: 0.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-01 19:46:40 -07:00
Frédéric Guillot f99dff5238 docs(changelog): update release notes for version 2.2.7 2025-04-01 17:18:32 -07:00
Frédéric Guillot 1936ced1ed docs: update README 2025-03-30 11:18:16 -07:00
Frédéric Guillot cb695e653a fix(security): use a more restrictive CSP for untrusted content 2025-03-29 19:49:41 -07:00
Matthaiks f57949c9a2 feat(locale): update Polish translation 2025-03-29 15:15:21 -07:00
Frédéric Guillot 94fb3d6159 fix: change labels from Read / Unread to "Mark as Read" 2025-03-28 17:34:44 -07:00
Frédéric Guillot 51560f191f fix(subscription): add /rss/feed.xml to the list of known feed URLs 2025-03-28 16:59:06 -07:00
Frédéric Guillot c0fcdf3b6e fix(ui): update entry tags display logic to show links based on user authentication 2025-03-28 15:53:08 -07:00
Frédéric Guillot 969efd2af7 fix(ui): update share feature to correctly select title element and handle empty title 2025-03-28 15:36:43 -07:00
Frédéric Guillot 87fccfee3a fix(ui): remove touch-action style because it could cause horizontal scrolling issue 2025-03-28 15:20:31 -07:00
Frédéric Guillot e643effefa feat: combine feed icon handlers to use only externalIconID 2025-03-28 14:29:19 -07:00
Frédéric Guillot c531be8780 fix: update Content-Security-Policy to use 'sandbox' directive 2025-03-28 13:06:59 -07:00
Frédéric Guillot 6e3cecc57e feat(locale): add Romanian translation
This contribution was sent outside GitHub. Credit to Laurentiu.
2025-03-28 11:11:20 -07:00
Tali Auster b3d385861f fix: log warning on an empty client secret
Because Miniflux runs as a confidential service, a missing client secret
is a mistake in configuration. An empty client secret appears to be
valid per RFC 6749 (and is in fact the default set by Miniflux!), so we
log a warning.
2025-03-27 20:00:09 -07:00
dependabot[bot] cc5a8fa4b3 build(deps): bump golang.org/x/net from 0.37.0 to 0.38.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-27 17:26:54 -07:00
Frédéric Guillot 78ff727064 fix: name "external_id": converting NULL to string is unsupported
Regresion introduced by commit df8bc742fb.
2025-03-26 20:51:57 -07:00
dependabot[bot] 54dad707a4 build(deps): bump github.com/tdewolff/minify/v2 from 2.22.3 to 2.22.4
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.22.3 to 2.22.4.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.22.3...v2.22.4)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-26 19:48:23 -07:00
Josiah Campbell df8bc742fb feat(googlereader): add feed icon field, endpoint
Adds an endpoint to the Google Reader integration to serve
feed icon URLs.
2025-03-25 20:55:16 -07:00
Frédéric Guillot e342a4f143 fix: address minor issues detected by Go linters 2025-03-24 20:48:46 -07:00
dependabot[bot] febb7b1748 build(deps): bump golangci/golangci-lint-action from 6 to 7
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6 to 7.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 20:02:03 -07:00
Frédéric Guillot 537867a64b ci: replace GitHub Issue Markdown templates with YAML forms 2025-03-24 17:27:31 -07:00
Frédéric Guillot 82a6fe64ae feat(storage): reduce the number of SQL queries when fetching entry enclosures 2025-03-23 14:13:33 -07:00
dependabot[bot] 984a66b590 build(deps): bump github.com/golang-jwt/jwt/v5 from 5.2.1 to 5.2.2
Bumps [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) from 5.2.1 to 5.2.2.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v5.2.1...v5.2.2)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-21 15:33:19 -07:00
dependabot[bot] bd79ad2a7d build(deps): bump github.com/tdewolff/minify/v2 from 2.22.2 to 2.22.3
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.22.2 to 2.22.3.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.22.2...v2.22.3)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-20 20:26:38 -07:00
dependabot[bot] b583de88f3 build(deps): bump github.com/tdewolff/minify/v2 from 2.21.3 to 2.22.2
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.21.3 to 2.22.2.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.21.3...v2.22.2)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-14 17:57:04 -07:00
dependabot[bot] baccb9e719 build(deps): bump github.com/coreos/go-oidc/v3 from 3.12.0 to 3.13.0
Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc) from 3.12.0 to 3.13.0.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v3.12.0...v3.13.0)

---
updated-dependencies:
- dependency-name: github.com/coreos/go-oidc/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-13 19:30:26 -07:00
dependabot[bot] 0fd11693f3 build(deps): bump github.com/go-webauthn/webauthn from 0.12.1 to 0.12.2 (#3209)
Bumps [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) from 0.12.1 to 0.12.2.
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.12.1...v0.12.2)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 18:02:27 -07:00
Matthaiks 05f7a34d43 feat(locale): update Polish translation 2025-03-07 11:36:25 -08:00
Frédéric Guillot 315e72c412 fix(rewrite): remove obsolete rule for webtoons.com 2025-03-06 20:11:03 -08:00
jvoisin f916373f55 fix: allow the <b> tag 2025-03-06 19:27:30 -08:00
jvoisin 5353211206 fix: allow the <u> tag in feeds 2025-03-06 19:26:26 -08:00
dependabot[bot] 999ab0df2b build(deps): bump golang.org/x/net from 0.36.0 to 0.37.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.36.0 to 0.37.0.
- [Commits](https://github.com/golang/net/compare/v0.36.0...v0.37.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 16:29:56 -08:00
dependabot[bot] f8700c84b8 build(deps): bump golang.org/x/image from 0.24.0 to 0.25.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/image/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 16:22:29 -08:00
dependabot[bot] ddd916161e build(deps): bump golang.org/x/crypto from 0.35.0 to 0.36.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/crypto/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 16:16:27 -08:00
dependabot[bot] c3cd5e3109 build(deps): bump golang.org/x/oauth2 from 0.27.0 to 0.28.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.27.0 to 0.28.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 16:11:59 -08:00
dependabot[bot] 051441d875 build(deps): bump golang.org/x/term from 0.29.0 to 0.30.0
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.29.0 to 0.30.0.
- [Commits](https://github.com/golang/term/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-05 15:53:41 -08:00
Paul Stenius 0cf5051cf8 feat: show size of DB on the about page 2025-03-05 15:49:32 -08:00
Paul Stenius 12ca7aa085 feat: add a make add string command to add new localized strings 2025-03-04 21:22:33 -08:00
dependabot[bot] d53dc4cf28 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.0 to 1.21.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.21.0...v1.21.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 16:47:52 -08:00
dependabot[bot] e7298b255a build(deps): bump golang.org/x/net from 0.35.0 to 0.36.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/net/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-04 16:45:39 -08:00
AiraNadih ad02f21d04 refactor(rewrite): reorganize referer rules and remove obsolete mapping 2025-03-02 19:40:52 -08:00
Frédéric Guillot 46323b3fe7 test(api): update base url after upgrading hugo 2025-03-02 19:32:09 -08:00
David Pedersen 86b3593fec docs: update client README to not reference deprecated function 2025-02-27 17:52:08 -08:00
Frédéric Guillot d7c504f48e fix(ui): avoid 500 error and NaN when marking as read a deleted entry
Steps to reproduce:

1. In /unread, open a feed's settings in a new tab. The feed must have unread entries in /unread.
2. In the new tab/window, delete the feed.
3. Without refreshing, mark an entry from the now-deleted feed as read.
4. Result: The total unread count in the UI header switches to NaN.
2025-02-26 16:39:38 -08:00
Matthaiks 6ad2001c7d feat(locale): update Polish translation 2025-02-26 16:33:18 -08:00
dependabot[bot] c5d4c762d8 build(deps): bump golang.org/x/crypto from 0.33.0 to 0.35.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.33.0 to 0.35.0.
- [Commits](https://github.com/golang/crypto/compare/v0.33.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 16:59:46 -08:00
dependabot[bot] 8b66c2be1d build(deps): bump golang.org/x/oauth2 from 0.26.0 to 0.27.0
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.26.0 to 0.27.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 16:58:57 -08:00
dependabot[bot] cfb22d7ec1 build(deps): bump github.com/go-webauthn/webauthn from 0.11.2 to 0.12.1
Bumps [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) from 0.11.2 to 0.12.1.
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.11.2...v0.12.1)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 16:54:07 -08:00
Maytham Alsudany f01ff067a5 fix(processor): add missing quotation marks to import comments 2025-02-24 16:34:26 -08:00
Uziskull 55a3f9fcc9 feat(integrations/ntfy): make ntfy topics configurable per feed, with default one as fallback 2025-02-24 16:29:08 -08:00
dependabot[bot] 996f6f68d2 build(deps): bump github.com/go-jose/go-jose/v4 from 4.0.2 to 4.0.5
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.2 to 4.0.5.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.0.2...v4.0.5)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 16:03:48 -08:00
Matthaiks 3b8ac8b16a feat(locale): update Polish translation 2025-02-23 11:46:28 -08:00
Frédéric Guillot 381f03b56a fix(greader): return enclosures in streamItemContentsHandler response 2025-02-22 20:11:41 -08:00
jvoisin 117d711d7d feat(urlcleaner): add more Google Analytics parameters 2025-02-22 17:07:59 -08:00
dependabot[bot] 2f71804ff4 build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.5 to 1.21.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.5...v1.21.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-22 17:06:47 -08:00
140 changed files with 3117 additions and 668 deletions
-10
View File
@@ -1,10 +0,0 @@
---
name: Bug report
about: Create a bug report
title: ''
labels: bug, triage needed
assignees: ''
---
+105
View File
@@ -0,0 +1,105 @@
name: "Bug Report"
description: "Report a bug or unexpected behavior"
title: "[Bug]: "
type: "Bug"
labels: ["triage needed"]
body:
- type: markdown
attributes:
value: |
Thanks for reporting a bug! Please provide detailed information to help us reproduce and fix the issue.
- type: input
id: summary
attributes:
label: "Bug Summary"
description: "Briefly describe the bug."
placeholder: "e.g., Error when saving a new entry"
validations:
required: true
- type: textarea
id: description
attributes:
label: "Description"
description: "A clear and concise description of the bug."
placeholder: "e.g., When I click 'Save', I get a 500 error."
validations:
required: true
- type: textarea
id: steps_to_reproduce
attributes:
label: "Steps to Reproduce"
description: "Steps to reproduce the behavior."
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
validations:
required: true
- type: textarea
id: expected_behavior
attributes:
label: "Expected Behavior"
description: "What should happen instead?"
placeholder: "e.g., The form should be saved successfully."
validations:
required: true
- type: textarea
id: actual_behavior
attributes:
label: "Actual Behavior"
description: "What actually happens?"
placeholder: "e.g., A 500 error is returned with no useful error message."
validations:
required: true
- type: input
id: version
attributes:
label: "Version"
description: "Which version of Miniflux are you using?"
placeholder: "e.g., 2.2.6"
validations:
required: true
- type: input
id: browser
attributes:
label: "Browser"
description: "If applicable, which browser are you using? Please provide the version."
placeholder: "e.g., Chrome, Firefox, Safari"
validations:
required: false
- type: textarea
id: logs
attributes:
label: "Relevant Logs or Error Output"
description: "Paste any relevant logs or error messages (if applicable)."
render: shell
placeholder: "e.g., Stack trace, log files, browser console logs, or console output"
validations:
required: false
- type: textarea
id: additional_context
attributes:
label: "Additional Context"
description: "Add any other context about the problem here."
placeholder: "e.g., Screenshots, video recordings, or related issues"
validations:
required: false
- type: checkboxes
id: agreement
attributes:
label: "Checklist"
description: "Please confirm the following:"
options:
- label: "I have searched existing issues to ensure this bug hasn't been reported before."
required: true
+81
View File
@@ -0,0 +1,81 @@
name: "Documentation Issue"
description: "Report issues or suggest improvements for the documentation"
title: "[Docs]: "
type: "Documentation"
labels: ["triage needed"]
body:
- type: markdown
attributes:
value: |
Thanks for helping improve the Miniflux documentation! Clear and accurate documentation helps everyone.
- type: dropdown
id: issue_type
attributes:
label: "Documentation Issue Type"
description: "What kind of documentation issue are you reporting?"
options:
- "Missing Information"
- "Incorrect Information"
- "Outdated Information"
- "Unclear Explanation"
- "Formatting/Structural Issue"
- "Typo/Grammar Error"
- "Documentation Request"
- "Other"
validations:
required: true
- type: input
id: summary
attributes:
label: "Summary"
description: "Briefly describe the documentation issue."
placeholder: "e.g., The API authentication section is outdated"
validations:
required: true
- type: input
id: location
attributes:
label: "Location"
description: "Where is the documentation you're referring to? Provide URLs, file paths, or section names."
placeholder: "e.g., README.md, docs/api.md, Installation section of the website"
validations:
required: true
- type: textarea
id: description
attributes:
label: "Detailed Description"
description: "Provide a detailed description of the issue or improvement."
placeholder: "e.g., The API authentication section doesn't mention the new token-based authentication method introduced in version 2.0.5."
validations:
required: true
- type: textarea
id: current_content
attributes:
label: "Current Content (if applicable)"
description: "What does the current documentation say?"
placeholder: "Paste the current documentation text here."
validations:
required: false
- type: textarea
id: suggested_content
attributes:
label: "Suggested Changes"
description: "If you have specific suggestions for how to improve the documentation, please provide them here."
placeholder: "e.g., Add a new section about token-based authentication with these details..."
validations:
required: false
- type: input
id: version
attributes:
label: "Version"
description: "Which version of Miniflux does this documentation issue relate to?"
placeholder: "e.g., 2.2.6, or 'all versions'"
validations:
required: false
-10
View File
@@ -1,10 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: wishlist
assignees: ''
---
- [ ] I have read this document: https://miniflux.app/opinionated.html#feature-request
@@ -0,0 +1,65 @@
name: "Feature Request"
description: "Suggest an idea or improvement for the project"
title: "[Feature]: "
type: "Feature"
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to suggest a feature! Please provide detailed information to help us understand and evaluate your idea.
- type: input
id: summary
attributes:
label: "Feature Summary"
description: "Briefly describe the feature or enhancement."
placeholder: "e.g., Add dark mode support"
validations:
required: true
- type: textarea
id: problem
attributes:
label: "What problem does this feature solve?"
description: "Explain the problem or limitation this feature would address."
placeholder: "e.g., It's difficult to use the app in low-light environments."
validations:
required: true
- type: textarea
id: solution
attributes:
label: "Proposed Solution"
description: "Describe how you think this feature should work."
placeholder: "e.g., Add a toggle in settings to switch between light and dark mode."
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: "Alternatives Considered"
description: "Have you considered other solutions or workarounds?"
placeholder: "e.g., Using browser extensions to force dark mode."
validations:
required: false
- type: textarea
id: additional_context
attributes:
label: "Additional Context"
description: "Add any other context, screenshots, or examples to explain your request."
placeholder: "e.g., A screenshot of a similar feature in another project."
validations:
required: false
- type: checkboxes
id: agreement
attributes:
label: "Checklist"
description: "Please confirm the following:"
options:
- label: "I have searched existing issues to ensure this feature hasn't been requested before."
required: true
- label: "I understand that feature requests are not guaranteed to be implemented."
required: true
-10
View File
@@ -1,10 +0,0 @@
---
name: Feed Problems
about: Problems with a feed or a website
title: ''
labels: feed problems, triage needed
assignees: ''
---
+84
View File
@@ -0,0 +1,84 @@
name: "Feed/Website Issue"
description: "Report problems with a specific feed or website"
title: "[Feed Issue]: "
type: "Feed Issue"
labels: ["triage needed"]
body:
- type: markdown
attributes:
value: |
Thanks for reporting an issue with a feed or website! Please provide detailed information to help us diagnose and resolve the problem.
- type: input
id: feed_url
attributes:
label: "Feed URL"
description: "Provide the URL of the feed that is not working correctly."
placeholder: "e.g., https://example.com/feed.xml"
validations:
required: true
- type: input
id: website_url
attributes:
label: "Website URL"
description: "Provide the URL of the website."
placeholder: "e.g., https://example.com"
validations:
required: true
- type: textarea
id: problem_description
attributes:
label: "Problem Description"
description: "Describe the issue you are experiencing with this feed."
placeholder: |
e.g.,
- The feed URL returns a 403 error.
- The content is malformed.
- Images are not loading in the web ui.
validations:
required: true
- type: textarea
id: expected_behavior
attributes:
label: "Expected Behavior"
description: "Describe what you expect to happen."
placeholder: "e.g., The feed should show the images correctly."
validations:
required: true
- type: textarea
id: error_logs
attributes:
label: "Relevant Logs or Error Output"
description: "Paste any relevant logs or error messages, if available."
render: shell
placeholder: "e.g., HTTP error codes, invalid XML warnings, etc."
validations:
required: false
- type: textarea
id: additional_context
attributes:
label: "Additional Context"
description: "Add any other context, screenshots, or related information to help us troubleshoot."
placeholder: "e.g., Is this a recurring problem? Did the feed work before?"
validations:
required: false
- type: checkboxes
id: troubleshooting
attributes:
label: "Troubleshooting Steps"
description: "Please confirm that you have tried the following:"
options:
- label: "I have checked if the feed URL is correct and accessible in a web browser."
required: true
- label: "I have checked if the feed URL is correct and accessible with `curl`."
required: true
- label: "I have verified that the feed is valid using an RSS/Atom validator."
required: false
- label: "I have searched for existing issues to avoid duplicates."
required: true
+86
View File
@@ -0,0 +1,86 @@
name: "Proposal / RFC"
description: "Propose a significant change, or architectural decision"
title: "[Proposal]: "
type: "Proposal"
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to submit a proposal! Please provide detailed information to ensure a productive discussion.
- type: input
id: summary
attributes:
label: "Proposal Summary"
description: "A brief summary of the proposed change or idea."
placeholder: "e.g., Refactor database schema for performance optimization"
validations:
required: true
- type: textarea
id: motivation
attributes:
label: "Motivation and Context"
description: "Explain the problem this proposal addresses. Why is it necessary? What are the current limitations or pain points?"
placeholder: |
e.g.,
- The current database schema causes performance bottlenecks when querying large datasets.
- Adding this feature will improve scalability and reliability for large-scale use cases.
validations:
required: true
- type: textarea
id: proposed_solution
attributes:
label: "Proposed Solution"
description: "Describe the proposed solution or approach. Include technical details, diagrams, and examples where possible."
placeholder: |
e.g.,
- Redesign the schema to normalize tables and introduce indexing.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: "Alternatives Considered"
description: "List any alternative approaches that were considered and explain why they were rejected."
placeholder: |
e.g.,
- Use Redis for caching, but it adds operational complexity.
- Stick with the current schema and optimize queries, but this has limited impact on performance.
validations:
required: false
- type: textarea
id: impact
attributes:
label: "Impact and Risks"
description: "Describe the potential impact of this change. Highlight possible risks and backward compatibility concerns."
placeholder: |
e.g.,
- May require data migration with downtime.
- Could introduce breaking changes in API responses.
- Affects core functionality, requiring extensive testing.
validations:
required: true
- type: textarea
id: additional_context
attributes:
label: "Additional Context or References"
description: "Add any relevant context, links to related discussions, RFCs, or design documents."
placeholder: "e.g., Links to research, GitHub issues, or similar projects"
validations:
required: false
- type: checkboxes
id: agreement
attributes:
label: "Checklist"
description: "Please confirm the following:"
options:
- label: "I have reviewed existing proposals to ensure this change hasn't been proposed before."
required: true
- label: "I agree to provide follow-up updates and maintain discussion on this proposal."
required: true
+9 -15
View File
@@ -29,32 +29,26 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: "1.24.x"
- uses: golangci/golangci-lint-action@v6
- uses: golangci/golangci-lint-action@v7
with:
args: >
--timeout 10m
--exclude-dirs=tests
--disable errcheck
--enable sqlclosecheck,misspell,gofmt,goimports,whitespace,gocritic
- uses: dominikh/staticcheck-action@v1.3.1
with:
version: "latest"
install-go: false
--enable sqlclosecheck,misspell,whitespace,gocritic
- name: Run gofmt linter
run: gofmt -d -e .
commitlint:
if: github.event_name == 'pull_request'
name: Commit Linter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
node-version: "lts/*"
- name: Install commitlint
run: |
npm install --save-dev @commitlint/config-conventional @commitlint/cli
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
python-version: '3.13'
- name: Validate PR commits
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
run: python3 .github/workflows/scripts/commit-checker.py --base ${{ github.event.pull_request.base.sha }} --head ${{ github.event.pull_request.head.sha }}
@@ -0,0 +1,93 @@
import subprocess
import re
import sys
import argparse
from typing import Match
# Conventional commit pattern
CONVENTIONAL_COMMIT_PATTERN: str = r"^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9-]+\))?!?: .{1,100}"
def get_commit_message(commit_hash: str) -> str:
"""Get the commit message for a given commit hash."""
try:
result: subprocess.CompletedProcess = subprocess.run(
["git", "show", "-s", "--format=%B", commit_hash],
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"Error retrieving commit message: {e}")
sys.exit(1)
def check_commit_message(
message: str, pattern: str = CONVENTIONAL_COMMIT_PATTERN
) -> bool:
"""Check if commit message follows conventional commit format."""
first_line: str = message.split("\n")[0]
match: Match[str] | None = re.match(pattern, first_line)
return bool(match)
def check_commit_range(base_ref: str, head_ref: str) -> list[dict[str, str]]:
"""Check all commits in a range for compliance."""
try:
result: subprocess.CompletedProcess = subprocess.run(
["git", "log", "--format=%H", f"{base_ref}..{head_ref}"],
capture_output=True,
text=True,
check=True,
)
commit_hashes: list[str] = result.stdout.strip().split("\n")
# Filter out empty lines
commit_hashes = [hash for hash in commit_hashes if hash]
non_compliant: list[dict[str, str]] = []
for commit_hash in commit_hashes:
message: str = get_commit_message(commit_hash)
if not check_commit_message(message):
non_compliant.append(
{"hash": commit_hash, "message": message.split("\n")[0]}
)
return non_compliant
except subprocess.CalledProcessError as e:
print(f"Error checking commit range: {e}")
sys.exit(1)
def main() -> None:
parser: argparse.ArgumentParser = argparse.ArgumentParser(
description="Check conventional commit compliance"
)
parser.add_argument(
"--base", required=True, help="Base ref (starting commit, exclusive)"
)
parser.add_argument(
"--head", required=True, help="Head ref (ending commit, inclusive)"
)
args: argparse.Namespace = parser.parse_args()
non_compliant: list[dict[str, str]] = check_commit_range(args.base, args.head)
if non_compliant:
print("The following commits do not follow the conventional commit format:")
for commit in non_compliant:
print(f"- {commit['hash'][:8]}: {commit['message']}")
print("\nPlease ensure your commit messages follow the format:")
print("type(scope): subject")
print(
"\nWhere type is one of: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test"
)
sys.exit(1)
else:
print("All commits follow the conventional commit format!")
sys.exit(0)
if __name__ == "__main__":
main()
+74
View File
@@ -1,3 +1,77 @@
Version 2.2.8 (April 22, 2025)
------------------------------
* refactor(js): replace `DomHelper` methods with standalone functions
* refactor: avoid logging twice the feed errors in the background worker
* fix(api): `hide_globally` categories field should be a boolean
* fix(ui): add missing `await` when calling `navigator.share()` method
* fix(ui): replace share link with a form button for better accessibility
* feat(telegrambot): replace "Go to website" button with "Go to Miniflux"
* feat(locale): update Polish translation
* feat(locale): update German translation
* feat(locale): update Chinese translation
* feat(config): add `SCHEDULER_ROUND_ROBIN_MAX_INTERVAL` option
* feat(cli): add `-reset-feed-next-check-at` argument
* feat(api): add `update_content` query parameter to `/entries/{entryID}/fetch-content` endpoint
* feat: use `Cache-Control` max-age and `Expires` headers to calculate next check
* feat: implement proxy URL per feed
* feat: add proxy rotation functionality
* ci(linter): replace commitlint with a Python script
* ci: add documentation issue template
* build(deps): bump `golang.org/x/oauth2` from `0.28.0` to `0.29.0`
* build(deps): bump `golang.org/x/net` from `0.38.0` to `0.39.0`
* build(deps): bump `golang.org/x/image` from `0.25.0` to `0.26.0`
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.22.4` to `2.23.1`
* build(deps): bump `github.com/PuerkitoBio/goquery` from `1.10.2` to `1.10.3`
* build(deps): bump `github.com/prometheus/client_golang`
* build(deps): bump `github.com/mattn/go-sqlite3` from `1.14.24` to `1.14.28`
* build(deps): bump `github.com/go-webauthn/webauthn` from `0.12.2` to `0.12.3`
* build(deps): bump `github.com/coreos/go-oidc/v3` from `3.13.0` to `3.14.1`
Version 2.2.7 (April 1, 2025)
-----------------------------
* test(api): update base URL after upgrading Hugo
* refactor(rewrite): reorganize referer rules and remove obsolete mappings
* refactor: combine feed icon handlers to use only `externalIconID`
* fix(ui): update share feature to correctly select the title element and handle empty titles
* fix(ui): update entry tags display logic to show links based on user authentication
* fix(ui): remove touch-action style to prevent horizontal scrolling issues
* fix(ui): log a warning for an empty client secret
* fix(ui): change labels from "Read / Unread" to "Mark as Read"
* fix(ui): avoid 500 errors and NaN when marking a deleted entry as read
* fix(subscription): add `/rss/feed.xml` to the list of known feed URLs
* fix(security): use a more restrictive CSP for untrusted content
* fix(rewrite): remove obsolete rule for `webtoons.com`
* fix(processor): add missing quotation marks to import comments
* fix(googlereader): return enclosures in the `streamItemContentsHandler` response
* fix: address minor issues detected by Go linters
* feat(urlcleaner): add more Google Analytics parameters
* feat(storage): reduce the number of SQL queries when fetching entry enclosures
* feat(sanitizer): allow the `<u>` tag in feeds
* feat(sanitizer): allow the `<b>` tag
* feat(locale): update Polish translation
* feat(locale): add Romanian translation
* feat(integrations/ntfy): make ntfy topics configurable per feed
* feat(googlereader): add a feed icon endpoint
* feat: show database size on the about page
* feat: add a `make add string` command to add new localized strings
* docs: update README
* docs: update client README to remove references to deprecated functions
* ci: replace GitHub Issue Markdown templates with YAML forms
* build(deps): bump `golangci/golangci-lint-action` from `6` to `7`
* build(deps): bump `golang.org/x/term` from `0.29.0` to `0.30.0`
* build(deps): bump `golang.org/x/oauth2` from `0.26.0` to` 0.28.0`
* build(deps): bump `golang.org/x/net` from `0.35.0` to `0.38.0`
* build(deps): bump `golang.org/x/image` from `0.24.0` to `0.25.0`
* build(deps): bump `golang.org/x/crypto` from `0.33.0` to `0.36.0`
* build(deps): bump `github.com/tdewolff/minify/v2` from `2.21.3` to `2.22.4`
* build(deps): bump `github.com/prometheus/client_golang`
* build(deps): bump `github.com/golang-jwt/jwt/v5` from `5.2.1` to `5.2.2`
* build(deps): bump `github.com/go-webauthn/webauthn` from `0.11.2` to `0.12.2`
* build(deps): bump `github.com/go-jose/go-jose/v4` from `4.0.2` to `4.0.5`
* build(deps): bump `github.com/coreos/go-oidc/v3` from `3.12.0` to `3.13.0`
Version 2.2.6 (February 22, 2025)
---------------------------------
+11 -1
View File
@@ -116,6 +116,16 @@ run:
clean:
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb $(APP)*.exe $(APP)*.sha256
.PHONY: add-string
add-string:
cd internal/locale/translations && \
for file in *.json; do \
jq --indent 4 --arg key "$(KEY)" --arg val "$(VAL)" \
'. + {($$key): $$val} | to_entries | sort_by(.key) | from_entries' "$$file" > tmp && \
mv tmp "$$file"; \
done
test:
go test -cover -race -count=1 ./...
@@ -134,7 +144,7 @@ integration-test:
ADMIN_PASSWORD=test123 \
CREATE_ADMIN=1 \
RUN_MIGRATIONS=1 \
DEBUG=1 \
LOG_LEVEL=debug \
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
while ! nc -z localhost 8080; do sleep 1; done
+101 -11
View File
@@ -1,20 +1,110 @@
Miniflux 2
==========
Miniflux is a minimalist and opinionated feed reader:
- Written in Go (Golang)
- Works only with Postgresql
- Doesn't use any ORM
- Doesn't use any complicated framework
- Use only modern vanilla Javascript (ES6 and Fetch API)
- Single binary compiled statically without dependency
- The number of features is voluntarily limited
Miniflux is a minimalist and opinionated feed reader.
It's simple, fast, lightweight and super easy to install.
Official website: <https://miniflux.app>
Features
--------
### Feed Reader
- Supported feed formats: Atom 0.3/1.0, RSS 1.0/2.0, and JSON Feed 1.0/1.1.
- [OPML](https://en.wikipedia.org/wiki/OPML) file import/export and URL import.
- Supports multiple attachments (podcasts, videos, music, and images enclosures).
- Plays videos from YouTube directly inside Miniflux.
- Organizes articles using categories and bookmarks.
- Share individual articles publicly.
- Fetches website icons (favicons).
- Saves articles to third-party services.
- Provides full-text search (powered by Postgres).
- Available in 20 languages: Portuguese (Brazilian), Chinese (Simplified and Traditional), Dutch, English (US), Finnish, French, German, Greek, Hindi, Indonesian, Italian, Japanese, Polish, Romanian, Russian, Taiwanese POJ, Ukrainian, Spanish, and Turkish.
### Privacy
- Removes pixel trackers.
- Strips tracking parameters from URLs (e.g., `utm_source`, `utm_medium`, `utm_campaign`, `fbclid`, etc.).
- Retrieves original links when feeds are sourced from FeedBurner.
- Opens external links with attributes `rel="noopener noreferrer" referrerpolicy="no-referrer"` for improved security.
- Implements the HTTP header `Referrer-Policy: no-referrer` to prevent referrer leakage.
- Provides a media proxy to avoid tracking and resolve mixed content warnings when using HTTPS.
- Plays YouTube videos via the privacy-focused domain `youtube-nocookie.com`.
- Supports alternative YouTube video players such as [Invidious](https://invidio.us).
- Blocks external JavaScript to prevent tracking and enhance security.
### Bot Protection Bypass Mechanisms
- Optionally disable HTTP/2 to mitigate fingerprinting.
- Allows configuration of a custom user agent.
- Supports adding custom cookies for specific use cases.
- Enables the use of proxies for enhanced privacy or bypassing restrictions.
### Content Manipulation
- Fetches the original article and extracts only the relevant content using a local Readability parser.
- Allows custom scraper rules based on <abbr title="Cascading Style Sheets">CSS</abbr> selectors.
- Supports custom rewriting rules for content manipulation.
- Provides a regex filter to include or exclude articles based on specific patterns.
- Optionally permits self-signed or invalid certificates (disabled by default).
- Scrapes YouTube's website to retrieve video duration as read time or uses the YouTube API (disabled by default).
### User Interface
- Optimized stylesheet for readability.
- Responsive design that adapts seamlessly to desktop, tablet, and mobile devices.
- Minimalistic and distraction-free user interface.
- No requirement to download an app from Apple App Store or Google Play Store.
- Can be added directly to the home screen for quick access.
- Supports a wide range of keyboard shortcuts for efficient navigation.
- Optional touch gesture support for navigation on mobile devices.
- Custom stylesheets and JavaScript to personalize the user interface to your preferences.
- Themes:
- Light (Sans-Serif)
- Light (Serif)
- Dark (Sans-Serif)
- Dark (Serif)
- System (Sans-Serif) Automatically switches between Dark and Light themes based on system preferences.
- System (Serif)
### Integrations
- 25+ integrations with third-party services: [Apprise](https://github.com/caronc/apprise), [Betula](https://sr.ht/~bouncepaw/betula/), [Cubox](https://cubox.cc/), [Discord](https://discord.com/), [Espial](https://github.com/jonschoning/espial), [Instapaper](https://www.instapaper.com/), [LinkAce](https://www.linkace.org/), [Linkding](https://github.com/sissbruecker/linkding), [LinkWarden](https://linkwarden.app/), [Matrix](https://matrix.org), [Notion](https://www.notion.com/), [Ntfy](https://ntfy.sh/), [Nunux Keeper](https://keeper.nunux.org/), [Pinboard](https://pinboard.in/), [Pocket](https://getpocket.com/), [Pushover](https://pushover.net), [RainDrop](https://raindrop.io/), [Readeck](https://readeck.org/en/), [Readwise Reader](https://readwise.io/read), [RssBridge](https://rss-bridge.org/), [Shaarli](https://github.com/shaarli/Shaarli), [Shiori](https://github.com/go-shiori/shiori), [Slack](https://slack.com/), [Telegram](https://telegram.org), [Wallabag](https://www.wallabag.org/), etc.
- Bookmarklet for subscribing to websites directly from any web browser.
- Webhooks for real-time notifications or custom integrations.
- Compatibility with existing mobile applications using the Fever or Google Reader API.
- REST API with client libraries available in [Go](https://github.com/miniflux/v2/tree/main/client) and [Python](https://github.com/miniflux/python-client).
### Authentication
- Local username and password.
- Passkeys ([WebAuthn](https://en.wikipedia.org/wiki/WebAuthn)).
- Google (OAuth2).
- Generic OpenID Connect.
- Reverse-Proxy authentication.
### Technical Stuff
- Written in [Go (Golang)](https://golang.org/).
- Single binary compiled statically without dependency.
- Works only with [PostgreSQL](https://www.postgresql.org/).
- Does not use any ORM or any complicated frameworks.
- Uses modern vanilla JavaScript only when necessary.
- All static files are bundled into the application binary using the Go `embed` package.
- Supports the Systemd `sd_notify` protocol for process monitoring.
- Configures HTTPS automatically with Let's Encrypt.
- Allows the use of custom <abbr title="Secure Sockets Layer">SSL</abbr> certificates.
- Supports [HTTP/2](https://en.wikipedia.org/wiki/HTTP/2) when TLS is enabled.
- Updates feeds in the background using an internal scheduler or a traditional cron job.
- Sanitizes external content before rendering it.
- Enforces a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) that permits only application JavaScript and blocks inline scripts and styles.
- Uses native lazy loading for images and iframes.
- Compatible only with modern browsers.
- Adheres to the [Twelve-Factor App](https://12factor.net/) methodology.
- Provides official Debian/RPM packages and pre-built binaries.
- Publishes a Docker image to Docker Hub, GitHub Registry, and Quay.io Registry, with ARM architecture support.
Documentation
-------------
@@ -29,7 +119,7 @@ The Miniflux documentation is available here: <https://miniflux.app/docs/> ([Man
- [Command Line Usage](https://miniflux.app/docs/cli.html)
- [User Interface Usage](https://miniflux.app/docs/ui.html)
- [Keyboard Shortcuts](https://miniflux.app/docs/keyboard_shortcuts.html)
- [Integration with External Services](https://miniflux.app/docs/services.html)
- [Integration with External Services](https://miniflux.app/docs/#integrations)
- [Rewrite and Scraper Rules](https://miniflux.app/docs/rules.html)
- [API Reference](https://miniflux.app/docs/api.html)
- [Development](https://miniflux.app/docs/development.html)
+2 -2
View File
@@ -27,10 +27,10 @@ import (
func main() {
// Authentication with username/password:
client := miniflux.New("https://api.example.org", "admin", "secret")
client := miniflux.NewClient("https://api.example.org", "admin", "secret")
// Authentication with an API Key:
client := miniflux.New("https://api.example.org", "my-secret-token")
client := miniflux.NewClient("https://api.example.org", "my-secret-token")
// Fetch all feeds.
feeds, err := client.Feeds()
+36 -4
View File
@@ -18,6 +18,7 @@ type Client struct {
}
// New returns a new Miniflux client.
//
// Deprecated: use NewClient instead.
func New(endpoint string, credentials ...string) *Client {
return NewClient(endpoint, credentials...)
@@ -238,8 +239,8 @@ func (c *Client) Categories() (Categories, error) {
// CreateCategory creates a new category.
func (c *Client) CreateCategory(title string) (*Category, error) {
body, err := c.request.Post("/v1/categories", map[string]interface{}{
"title": title,
body, err := c.request.Post("/v1/categories", &CategoryCreationRequest{
Title: title,
})
if err != nil {
return nil, err
@@ -254,10 +255,25 @@ func (c *Client) CreateCategory(title string) (*Category, error) {
return category, nil
}
// CreateCategoryWithOptions creates a new category with options.
func (c *Client) CreateCategoryWithOptions(createRequest *CategoryCreationRequest) (*Category, error) {
body, err := c.request.Post("/v1/categories", createRequest)
if err != nil {
return nil, err
}
defer body.Close()
var category *Category
if err := json.NewDecoder(body).Decode(&category); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return category, nil
}
// UpdateCategory updates a category.
func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, error) {
body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), map[string]interface{}{
"title": title,
body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), &CategoryModificationRequest{
Title: SetOptionalField(title),
})
if err != nil {
return nil, err
@@ -272,6 +288,22 @@ func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, erro
return category, nil
}
// UpdateCategoryWithOptions updates a category with options.
func (c *Client) UpdateCategoryWithOptions(categoryID int64, categoryChanges *CategoryModificationRequest) (*Category, error) {
body, err := c.request.Put(fmt.Sprintf("/v1/categories/%d", categoryID), categoryChanges)
if err != nil {
return nil, err
}
defer body.Close()
var category *Category
if err := json.NewDecoder(body).Decode(&category); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return category, nil
}
// MarkCategoryAsRead marks all unread entries in a category as read.
func (c *Client) MarkCategoryAsRead(categoryID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/categories/%d/mark-all-as-read", categoryID), nil)
+21 -3
View File
@@ -97,9 +97,12 @@ type Users []User
// Category represents a feed category.
type Category struct {
ID int64 `json:"id,omitempty"`
Title string `json:"title,omitempty"`
UserID int64 `json:"user_id,omitempty"`
ID int64 `json:"id"`
Title string `json:"title"`
UserID int64 `json:"user_id,omitempty"`
HideGlobally bool `json:"hide_globally,omitempty"`
FeedCount *int `json:"feed_count,omitempty"`
TotalUnread *int `json:"total_unread,omitempty"`
}
func (c Category) String() string {
@@ -109,6 +112,18 @@ func (c Category) String() string {
// Categories represents a list of categories.
type Categories []*Category
// CategoryCreationRequest represents the request to create a category.
type CategoryCreationRequest struct {
Title string `json:"title"`
HideGlobally bool `json:"hide_globally"`
}
// CategoryModificationRequest represents the request to update a category.
type CategoryModificationRequest struct {
Title *string `json:"title"`
HideGlobally *bool `json:"hide_globally"`
}
// Subscription represents a feed subscription.
type Subscription struct {
Title string `json:"title"`
@@ -151,6 +166,7 @@ type Feed struct {
Category *Category `json:"category,omitempty"`
HideGlobally bool `json:"hide_globally"`
DisableHTTP2 bool `json:"disable_http2"`
ProxyURL string `json:"proxy_url"`
}
// FeedCreationRequest represents the request to create a feed.
@@ -172,6 +188,7 @@ type FeedCreationRequest struct {
KeeplistRules string `json:"keeplist_rules"`
HideGlobally bool `json:"hide_globally"`
DisableHTTP2 bool `json:"disable_http2"`
ProxyURL string `json:"proxy_url"`
}
// FeedModificationRequest represents the request to update a feed.
@@ -195,6 +212,7 @@ type FeedModificationRequest struct {
FetchViaProxy *bool `json:"fetch_via_proxy"`
HideGlobally *bool `json:"hide_globally"`
DisableHTTP2 *bool `json:"disable_http2"`
ProxyURL *string `json:"proxy_url"`
}
// FeedIcon represents the feed icon.
+24 -23
View File
@@ -3,46 +3,47 @@ module miniflux.app/v2
// +heroku goVersion go1.23
require (
github.com/PuerkitoBio/goquery v1.10.2
github.com/PuerkitoBio/goquery v1.10.3
github.com/andybalholm/brotli v1.1.1
github.com/coreos/go-oidc/v3 v3.12.0
github.com/go-webauthn/webauthn v0.11.2
github.com/coreos/go-oidc/v3 v3.14.1
github.com/go-webauthn/webauthn v0.12.3
github.com/gorilla/mux v1.8.1
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.24
github.com/prometheus/client_golang v1.20.5
github.com/tdewolff/minify/v2 v2.21.3
golang.org/x/crypto v0.33.0
golang.org/x/image v0.24.0
golang.org/x/net v0.35.0
golang.org/x/oauth2 v0.26.0
golang.org/x/term v0.29.0
github.com/mattn/go-sqlite3 v1.14.28
github.com/prometheus/client_golang v1.22.0
github.com/tdewolff/minify/v2 v2.23.1
golang.org/x/crypto v0.37.0
golang.org/x/image v0.26.0
golang.org/x/net v0.39.0
golang.org/x/oauth2 v0.29.0
golang.org/x/term v0.31.0
)
require (
github.com/go-webauthn/x v0.1.14 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/google/go-tpm v0.9.1 // indirect
github.com/go-webauthn/x v0.1.20 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/go-tpm v0.9.3 // indirect
)
require (
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/tdewolff/parse/v2 v2.7.19 // indirect
github.com/tdewolff/parse/v2 v2.7.23 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
)
go 1.23
go 1.23.0
toolchain go1.24.1
+50 -50
View File
@@ -1,5 +1,5 @@
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
@@ -8,59 +8,59 @@ 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc=
github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0=
github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0=
github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-webauthn/webauthn v0.12.3 h1:hHQl1xkUuabUU9uS+ISNCMLs9z50p9mDUZI/FmkayNE=
github.com/go-webauthn/webauthn v0.12.3/go.mod h1:4JRe8Z3W7HIw8NGEWn2fnUwecoDzkkeach/NnvhkqGY=
github.com/go-webauthn/x v0.1.20 h1:brEBDqfiPtNNCdS/peu8gARtq8fIPsHz0VzpPjGvgiw=
github.com/go-webauthn/x v0.1.20/go.mod h1:n/gAc8ssZJGATM0qThE+W+vfgXiMedsWi3wf/C4lld0=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
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.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
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/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.21.3 h1:KmhKNGrN/dGcvb2WDdB5yA49bo37s+hcD8RiF+lioV8=
github.com/tdewolff/minify/v2 v2.21.3/go.mod h1:iGxHaGiONAnsYuo8CRyf8iPUcqRJVB/RhtEcTpqS7xw=
github.com/tdewolff/parse/v2 v2.7.19 h1:7Ljh26yj+gdLFEq/7q9LT4SYyKtwQX4ocNrj45UCePg=
github.com/tdewolff/parse/v2 v2.7.19/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.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.23.1 h1:r6sKQrumHzskWZRdhiRa+pZhn7CdBMojACNP9fuKpXQ=
github.com/tdewolff/minify/v2 v2.23.1/go.mod h1:RkUGjklq6uIsBoOdzY3ll35HKKQ2aFqLQhnanBHhDyU=
github.com/tdewolff/parse/v2 v2.7.23 h1:sCW2PNTCM1yVldh5YK/8wrpRI9rSbloUZWjAydlN2IA=
github.com/tdewolff/parse/v2 v2.7.23/go.mod h1:I7TXO37t3aSG9SlPUBefAhgIF8nt7yYUwVGgETIoBcA=
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
@@ -72,10 +72,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
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.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -90,10 +90,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -112,8 +112,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
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=
@@ -123,8 +123,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -134,8 +134,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@@ -143,7 +143,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+133 -2
View File
@@ -48,7 +48,7 @@ func newIntegrationTestConfig() *integrationTestConfig {
testFeedURL: getDefaultEnvValues("TEST_MINIFLUX_FEED_URL", "https://miniflux.app/feed.xml"),
testFeedTitle: getDefaultEnvValues("TEST_MINIFLUX_FEED_TITLE", "Miniflux"),
testSubscriptionTitle: getDefaultEnvValues("TEST_MINIFLUX_SUBSCRIPTION_TITLE", "Miniflux Releases"),
testWebsiteURL: getDefaultEnvValues("TEST_MINIFLUX_WEBSITE_URL", "https://miniflux.app"),
testWebsiteURL: getDefaultEnvValues("TEST_MINIFLUX_WEBSITE_URL", "https://miniflux.app/"),
}
}
@@ -824,6 +824,10 @@ func TestCreateCategoryEndpoint(t *testing.T) {
if category.Title != categoryName {
t.Errorf(`Invalid title, got "%v" instead of "%v"`, category.Title, categoryName)
}
if category.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, category.HideGlobally)
}
}
func TestCreateCategoryWithEmptyTitle(t *testing.T) {
@@ -865,7 +869,49 @@ func TestCannotCreateDuplicatedCategory(t *testing.T) {
}
}
func TestUpdateCatgoryEndpoint(t *testing.T) {
func TestCreateCategoryWithOptions(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
}
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
if err != nil {
t.Fatal(err)
}
defer adminClient.DeleteUser(regularTestUser.ID)
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
newCategory, err := regularUserClient.CreateCategoryWithOptions(&miniflux.CategoryCreationRequest{
Title: "My category",
HideGlobally: true,
})
if err != nil {
t.Fatalf(`Creating a category with options should not raise an error: %v`, err)
}
categories, err := regularUserClient.Categories()
if err != nil {
t.Fatal(err)
}
for _, category := range categories {
if category.ID == newCategory.ID {
if category.Title != newCategory.Title {
t.Errorf(`Invalid title, got %q instead of %q`, category.Title, newCategory.Title)
}
if category.HideGlobally != true {
t.Errorf(`Invalid hide globally value, got "%v"`, category.HideGlobally)
}
break
}
}
}
func TestUpdateCategoryEndpoint(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
@@ -903,6 +949,91 @@ func TestUpdateCatgoryEndpoint(t *testing.T) {
if updatedCategory.Title != "new title" {
t.Errorf(`Invalid title, got "%v" instead of "%v"`, updatedCategory.Title, "new title")
}
if updatedCategory.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
}
}
func TestUpdateCategoryWithOptions(t *testing.T) {
testConfig := newIntegrationTestConfig()
if !testConfig.isConfigured() {
t.Skip(skipIntegrationTestsMessage)
}
adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword)
regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false)
if err != nil {
t.Fatal(err)
}
defer adminClient.DeleteUser(regularTestUser.ID)
regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword)
newCategory, err := regularUserClient.CreateCategoryWithOptions(&miniflux.CategoryCreationRequest{
Title: "My category",
})
if err != nil {
t.Fatalf(`Creating a category with options should not raise an error: %v`, err)
}
updatedCategory, err := regularUserClient.UpdateCategoryWithOptions(newCategory.ID, &miniflux.CategoryModificationRequest{
Title: miniflux.SetOptionalField("new title"),
})
if err != nil {
t.Fatal(err)
}
if updatedCategory.ID != newCategory.ID {
t.Errorf(`Invalid categoryID, got "%v"`, updatedCategory.ID)
}
if updatedCategory.Title != "new title" {
t.Errorf(`Invalid title, got "%v" instead of "%v"`, updatedCategory.Title, "new title")
}
if updatedCategory.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
}
updatedCategory, err = regularUserClient.UpdateCategoryWithOptions(newCategory.ID, &miniflux.CategoryModificationRequest{
HideGlobally: miniflux.SetOptionalField(true),
})
if err != nil {
t.Fatal(err)
}
if updatedCategory.ID != newCategory.ID {
t.Errorf(`Invalid categoryID, got "%v"`, updatedCategory.ID)
}
if updatedCategory.Title != "new title" {
t.Errorf(`Invalid title, got "%v" instead of "%v"`, updatedCategory.Title, "new title")
}
if !updatedCategory.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
}
updatedCategory, err = regularUserClient.UpdateCategoryWithOptions(newCategory.ID, &miniflux.CategoryModificationRequest{
HideGlobally: miniflux.SetOptionalField(false),
})
if err != nil {
t.Fatal(err)
}
if updatedCategory.ID != newCategory.ID {
t.Errorf(`Invalid categoryID, got %q`, updatedCategory.ID)
}
if updatedCategory.Title != "new title" {
t.Errorf(`Invalid title, got %q instead of %q`, updatedCategory.Title, "new title")
}
if updatedCategory.HideGlobally {
t.Errorf(`Invalid hide globally value, got "%v"`, updatedCategory.HideGlobally)
}
}
func TestUpdateInexistingCategory(t *testing.T) {
+10 -10
View File
@@ -19,18 +19,18 @@ import (
func (h *handler) createCategory(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
var categoryRequest model.CategoryRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
var categoryCreationRequest model.CategoryCreationRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryCreationRequest); err != nil {
json.BadRequest(w, r, err)
return
}
if validationErr := validator.ValidateCategoryCreation(h.store, userID, &categoryRequest); validationErr != nil {
if validationErr := validator.ValidateCategoryCreation(h.store, userID, &categoryCreationRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
}
category, err := h.store.CreateCategory(userID, &categoryRequest)
category, err := h.store.CreateCategory(userID, &categoryCreationRequest)
if err != nil {
json.ServerError(w, r, err)
return
@@ -54,20 +54,20 @@ func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) {
return
}
var categoryRequest model.CategoryRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
var categoryModificationRequest model.CategoryModificationRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryModificationRequest); err != nil {
json.BadRequest(w, r, err)
return
}
if validationErr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryRequest); validationErr != nil {
if validationErr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryModificationRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
}
categoryRequest.Patch(category)
err = h.store.UpdateCategory(category)
if err != nil {
categoryModificationRequest.Patch(category)
if err := h.store.UpdateCategory(category); err != nil {
json.ServerError(w, r, err)
return
}
+12
View File
@@ -331,6 +331,18 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) {
return
}
shouldUpdateContent := request.QueryBoolParam(r, "update_content", false)
if shouldUpdateContent {
if err := h.store.UpdateEntryTitleAndContent(entry); err != nil {
json.ServerError(w, r, err)
return
}
json.OK(w, r, map[string]interface{}{"content": mediaproxy.RewriteDocumentWithRelativeProxyURL(h.router, entry.Content), "reading_time": entry.ReadingTime})
return
}
json.OK(w, r, map[string]string{"content": entry.Content})
}
+5 -2
View File
@@ -11,6 +11,7 @@ import (
"miniflux.app/v2/internal/http/request"
"miniflux.app/v2/internal/http/response/json"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
"miniflux.app/v2/internal/reader/subscription"
"miniflux.app/v2/internal/validator"
@@ -36,11 +37,13 @@ func (h *handler) discoverSubscriptions(w http.ResponseWriter, r *http.Request)
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(subscriptionDiscoveryRequest.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(subscriptionDiscoveryRequest.FetchViaProxy)
requestBuilder.WithUserAgent(subscriptionDiscoveryRequest.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(subscriptionDiscoveryRequest.Cookie)
requestBuilder.WithUsernameAndPassword(subscriptionDiscoveryRequest.Username, subscriptionDiscoveryRequest.Password)
requestBuilder.UseProxy(subscriptionDiscoveryRequest.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(subscriptionDiscoveryRequest.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(subscriptionDiscoveryRequest.DisableHTTP2)
+51 -30
View File
@@ -13,46 +13,49 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/database"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/storage"
"miniflux.app/v2/internal/ui/static"
"miniflux.app/v2/internal/version"
)
const (
flagInfoHelp = "Show build information"
flagVersionHelp = "Show application version"
flagMigrateHelp = "Run SQL migrations"
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
flagCreateAdminHelp = "Create an admin user from an interactive terminal"
flagResetPasswordHelp = "Reset user password"
flagResetFeedErrorsHelp = "Clear all feed errors for all users"
flagDebugModeHelp = "Show debug logs"
flagConfigFileHelp = "Load configuration file"
flagConfigDumpHelp = "Print parsed configuration values"
flagHealthCheckHelp = `Perform a health check on the given endpoint (the value "auto" try to guess the health check endpoint).`
flagRefreshFeedsHelp = "Refresh a batch of feeds and exit"
flagRunCleanupTasksHelp = "Run cleanup tasks (delete old sessions and archives old entries)"
flagExportUserFeedsHelp = "Export user feeds (provide the username as argument)"
flagInfoHelp = "Show build information"
flagVersionHelp = "Show application version"
flagMigrateHelp = "Run SQL migrations"
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
flagCreateAdminHelp = "Create an admin user from an interactive terminal"
flagResetPasswordHelp = "Reset user password"
flagResetFeedErrorsHelp = "Clear all feed errors for all users"
flagDebugModeHelp = "Show debug logs"
flagConfigFileHelp = "Load configuration file"
flagConfigDumpHelp = "Print parsed configuration values"
flagHealthCheckHelp = `Perform a health check on the given endpoint (the value "auto" try to guess the health check endpoint).`
flagRefreshFeedsHelp = "Refresh a batch of feeds and exit"
flagRunCleanupTasksHelp = "Run cleanup tasks (delete old sessions and archives old entries)"
flagExportUserFeedsHelp = "Export user feeds (provide the username as argument)"
flagResetNextCheckAtHelp = "Reset the next check time for all feeds"
)
// Parse parses command line arguments.
func Parse() {
var (
err error
flagInfo bool
flagVersion bool
flagMigrate bool
flagFlushSessions bool
flagCreateAdmin bool
flagResetPassword bool
flagResetFeedErrors bool
flagDebugMode bool
flagConfigFile string
flagConfigDump bool
flagHealthCheck string
flagRefreshFeeds bool
flagRunCleanupTasks bool
flagExportUserFeeds string
err error
flagInfo bool
flagVersion bool
flagMigrate bool
flagFlushSessions bool
flagCreateAdmin bool
flagResetPassword bool
flagResetFeedErrors bool
flagResetFeedNextCheckAt bool
flagDebugMode bool
flagConfigFile string
flagConfigDump bool
flagHealthCheck string
flagRefreshFeeds bool
flagRunCleanupTasks bool
flagExportUserFeeds string
)
flag.BoolVar(&flagInfo, "info", false, flagInfoHelp)
@@ -64,6 +67,7 @@ func Parse() {
flag.BoolVar(&flagCreateAdmin, "create-admin", false, flagCreateAdminHelp)
flag.BoolVar(&flagResetPassword, "reset-password", false, flagResetPasswordHelp)
flag.BoolVar(&flagResetFeedErrors, "reset-feed-errors", false, flagResetFeedErrorsHelp)
flag.BoolVar(&flagResetFeedNextCheckAt, "reset-feed-next-check-at", false, flagResetNextCheckAtHelp)
flag.BoolVar(&flagDebugMode, "debug", false, flagDebugModeHelp)
flag.StringVar(&flagConfigFile, "config-file", "", flagConfigFileHelp)
flag.StringVar(&flagConfigFile, "c", "", flagConfigFileHelp)
@@ -189,7 +193,16 @@ func Parse() {
}
if flagResetFeedErrors {
store.ResetFeedErrors()
if err := store.ResetFeedErrors(); err != nil {
printErrorAndExit(err)
}
return
}
if flagResetFeedNextCheckAt {
if err := store.ResetNextCheckAt(); err != nil {
printErrorAndExit(err)
}
return
}
@@ -228,6 +241,14 @@ func Parse() {
createAdminUserFromEnvironmentVariables(store)
}
if config.Opts.HasHTTPClientProxiesConfigured() {
slog.Info("Initializing proxy rotation", slog.Int("proxies_count", len(config.Opts.HTTPClientProxies())))
proxyrotator.ProxyRotatorInstance, err = proxyrotator.NewProxyRotator(config.Opts.HTTPClientProxies())
if err != nil {
printErrorAndExit(fmt.Errorf("unable to initialize proxy rotator: %v", err))
}
}
if flagRefreshFeeds {
refreshFeeds(store)
return
+103
View File
@@ -1028,6 +1028,41 @@ func TestSchedulerRoundRobin(t *testing.T) {
}
}
func TestDefaultSchedulerRoundRobinMaxIntervalValue(t *testing.T) {
os.Clearenv()
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := defaultSchedulerRoundRobinMaxInterval
result := opts.SchedulerRoundRobinMaxInterval()
if result != expected {
t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MAX_INTERVAL value, got %v instead of %v`, result, expected)
}
}
func TestSchedulerRoundRobinMaxInterval(t *testing.T) {
os.Clearenv()
os.Setenv("SCHEDULER_ROUND_ROBIN_MAX_INTERVAL", "150")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := 150
result := opts.SchedulerRoundRobinMaxInterval()
if result != expected {
t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MAX_INTERVAL value, got %v instead of %v`, result, expected)
}
}
func TestPollingParsingErrorLimit(t *testing.T) {
os.Clearenv()
os.Setenv("POLLING_PARSING_ERROR_LIMIT", "100")
@@ -2186,3 +2221,71 @@ func TestParseConfigDumpOutput(t *testing.T) {
t.Fatal(err)
}
}
func TestHTTPClientProxies(t *testing.T) {
os.Clearenv()
os.Setenv("HTTP_CLIENT_PROXIES", "http://proxy1.example.com,http://proxy2.example.com")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := []string{"http://proxy1.example.com", "http://proxy2.example.com"}
result := opts.HTTPClientProxies()
if len(expected) != len(result) {
t.Fatalf(`Unexpected HTTP_CLIENT_PROXIES value, got %v instead of %v`, result, expected)
}
for i, proxy := range expected {
if result[i] != proxy {
t.Fatalf(`Unexpected HTTP_CLIENT_PROXIES value at index %d, got %q instead of %q`, i, result[i], proxy)
}
}
}
func TestDefaultHTTPClientProxiesValue(t *testing.T) {
os.Clearenv()
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := []string{}
result := opts.HTTPClientProxies()
if len(expected) != len(result) {
t.Fatalf(`Unexpected default HTTP_CLIENT_PROXIES value, got %v instead of %v`, result, expected)
}
}
func TestHTTPClientProxy(t *testing.T) {
os.Clearenv()
os.Setenv("HTTP_CLIENT_PROXY", "http://proxy.example.com")
parser := NewParser()
opts, err := parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
expected := "http://proxy.example.com"
if opts.HTTPClientProxyURL() == nil || opts.HTTPClientProxyURL().String() != expected {
t.Fatalf(`Unexpected HTTP_CLIENT_PROXY value, got %v instead of %v`, opts.HTTPClientProxyURL(), expected)
}
}
func TestInvalidHTTPClientProxy(t *testing.T) {
os.Clearenv()
os.Setenv("HTTP_CLIENT_PROXY", "sche|me://invalid-proxy-url")
parser := NewParser()
_, err := parser.ParseEnvironmentVariables()
if err == nil {
t.Fatalf(`Expected error for invalid HTTP_CLIENT_PROXY value, but got none`)
}
}
+61 -12
View File
@@ -5,6 +5,7 @@ package config // import "miniflux.app/v2/internal/config"
import (
"fmt"
"net/url"
"sort"
"strings"
"time"
@@ -36,6 +37,7 @@ const (
defaultSchedulerEntryFrequencyMaxInterval = 24 * 60
defaultSchedulerEntryFrequencyFactor = 1
defaultSchedulerRoundRobinMinInterval = 60
defaultSchedulerRoundRobinMaxInterval = 1440
defaultPollingParsingErrorLimit = 3
defaultRunMigrations = false
defaultDatabaseURL = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
@@ -136,6 +138,7 @@ type Options struct {
schedulerEntryFrequencyMaxInterval int
schedulerEntryFrequencyFactor int
schedulerRoundRobinMinInterval int
schedulerRoundRobinMaxInterval int
pollingParsingErrorLimit int
workerPoolSize int
createAdmin bool
@@ -163,7 +166,8 @@ type Options struct {
pocketConsumerKey string
httpClientTimeout int
httpClientMaxBodySize int64
httpClientProxy string
httpClientProxyURL *url.URL
httpClientProxies []string
httpClientUserAgent string
httpServerTimeout int
authProxyHeader string
@@ -218,6 +222,7 @@ func NewOptions() *Options {
schedulerEntryFrequencyMaxInterval: defaultSchedulerEntryFrequencyMaxInterval,
schedulerEntryFrequencyFactor: defaultSchedulerEntryFrequencyFactor,
schedulerRoundRobinMinInterval: defaultSchedulerRoundRobinMinInterval,
schedulerRoundRobinMaxInterval: defaultSchedulerRoundRobinMaxInterval,
pollingParsingErrorLimit: defaultPollingParsingErrorLimit,
workerPoolSize: defaultWorkerPoolSize,
createAdmin: defaultCreateAdmin,
@@ -243,7 +248,8 @@ func NewOptions() *Options {
pocketConsumerKey: defaultPocketConsumerKey,
httpClientTimeout: defaultHTTPClientTimeout,
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
httpClientProxy: defaultHTTPClientProxy,
httpClientProxyURL: nil,
httpClientProxies: []string{},
httpClientUserAgent: defaultHTTPClientUserAgent,
httpServerTimeout: defaultHTTPServerTimeout,
authProxyHeader: defaultAuthProxyHeader,
@@ -430,6 +436,10 @@ func (o *Options) SchedulerRoundRobinMinInterval() int {
return o.schedulerRoundRobinMinInterval
}
func (o *Options) SchedulerRoundRobinMaxInterval() int {
return o.schedulerRoundRobinMaxInterval
}
// PollingParsingErrorLimit returns the limit of errors when to stop polling.
func (o *Options) PollingParsingErrorLimit() int {
return o.pollingParsingErrorLimit
@@ -587,9 +597,24 @@ func (o *Options) HTTPClientMaxBodySize() int64 {
return o.httpClientMaxBodySize
}
// HTTPClientProxy returns the proxy URL for HTTP client.
func (o *Options) HTTPClientProxy() string {
return o.httpClientProxy
// HTTPClientProxyURL returns the client HTTP proxy URL if configured.
func (o *Options) HTTPClientProxyURL() *url.URL {
return o.httpClientProxyURL
}
// HasHTTPClientProxyURLConfigured returns true if the client HTTP proxy URL if configured.
func (o *Options) HasHTTPClientProxyURLConfigured() bool {
return o.httpClientProxyURL != nil
}
// HTTPClientProxies returns the list of proxies.
func (o *Options) HTTPClientProxies() []string {
return o.httpClientProxies
}
// HTTPClientProxiesString returns true if the list of rotating proxies are configured.
func (o *Options) HasHTTPClientProxiesConfigured() bool {
return len(o.httpClientProxies) > 0
}
// HTTPServerTimeout returns the time limit in seconds before the HTTP server cancel the request.
@@ -597,11 +622,6 @@ func (o *Options) HTTPServerTimeout() int {
return o.httpServerTimeout
}
// HasHTTPClientProxyConfigured returns true if the HTTP proxy is configured.
func (o *Options) HasHTTPClientProxyConfigured() bool {
return o.httpClientProxy != ""
}
// AuthProxyHeader returns an HTTP header name that contains username for
// authentication using auth proxy.
func (o *Options) AuthProxyHeader() string {
@@ -664,6 +684,33 @@ func (o *Options) FilterEntryMaxAgeDays() int {
// SortedOptions returns options as a list of key value pairs, sorted by keys.
func (o *Options) SortedOptions(redactSecret bool) []*Option {
var clientProxyURLRedacted string
if o.httpClientProxyURL != nil {
if redactSecret {
clientProxyURLRedacted = o.httpClientProxyURL.Redacted()
} else {
clientProxyURLRedacted = o.httpClientProxyURL.String()
}
}
var clientProxyURLsRedacted string
if len(o.httpClientProxies) > 0 {
if redactSecret {
var proxyURLs []string
for range o.httpClientProxies {
proxyURLs = append(proxyURLs, "<redacted>")
}
clientProxyURLsRedacted = strings.Join(proxyURLs, ",")
} else {
clientProxyURLsRedacted = strings.Join(o.httpClientProxies, ",")
}
}
var mediaProxyPrivateKeyValue string
if len(o.mediaProxyPrivateKey) > 0 {
mediaProxyPrivateKeyValue = "<binary-data>"
}
var keyValues = map[string]interface{}{
"ADMIN_PASSWORD": redactSecretValue(o.adminPassword, redactSecret),
"ADMIN_USERNAME": o.adminUsername,
@@ -694,7 +741,8 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"FETCH_BILIBILI_WATCH_TIME": o.fetchBilibiliWatchTime,
"HTTPS": o.HTTPS,
"HTTP_CLIENT_MAX_BODY_SIZE": o.httpClientMaxBodySize,
"HTTP_CLIENT_PROXY": o.httpClientProxy,
"HTTP_CLIENT_PROXIES": clientProxyURLsRedacted,
"HTTP_CLIENT_PROXY": clientProxyURLRedacted,
"HTTP_CLIENT_TIMEOUT": o.httpClientTimeout,
"HTTP_CLIENT_USER_AGENT": o.httpClientUserAgent,
"HTTP_SERVER_TIMEOUT": o.httpServerTimeout,
@@ -729,7 +777,7 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"MEDIA_PROXY_HTTP_CLIENT_TIMEOUT": o.mediaProxyHTTPClientTimeout,
"MEDIA_PROXY_RESOURCE_TYPES": o.mediaProxyResourceTypes,
"MEDIA_PROXY_MODE": o.mediaProxyMode,
"MEDIA_PROXY_PRIVATE_KEY": redactSecretValue(string(o.mediaProxyPrivateKey), redactSecret),
"MEDIA_PROXY_PRIVATE_KEY": mediaProxyPrivateKeyValue,
"MEDIA_PROXY_CUSTOM_URL": o.mediaProxyCustomURL,
"ROOT_URL": o.rootURL,
"RUN_MIGRATIONS": o.runMigrations,
@@ -737,6 +785,7 @@ func (o *Options) SortedOptions(redactSecret bool) []*Option {
"SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": o.schedulerEntryFrequencyMinInterval,
"SCHEDULER_ENTRY_FREQUENCY_FACTOR": o.schedulerEntryFrequencyFactor,
"SCHEDULER_ROUND_ROBIN_MIN_INTERVAL": o.schedulerRoundRobinMinInterval,
"SCHEDULER_ROUND_ROBIN_MAX_INTERVAL": o.schedulerRoundRobinMaxInterval,
"SCHEDULER_SERVICE": o.schedulerService,
"SERVER_TIMING_HEADER": o.serverTimingHeader,
"WATCHDOG": o.watchdog,
+14 -3
View File
@@ -160,6 +160,8 @@ func (p *Parser) parseLines(lines []string) (err error) {
p.opts.schedulerEntryFrequencyFactor = parseInt(value, defaultSchedulerEntryFrequencyFactor)
case "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL":
p.opts.schedulerRoundRobinMinInterval = parseInt(value, defaultSchedulerRoundRobinMinInterval)
case "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL":
p.opts.schedulerRoundRobinMaxInterval = parseInt(value, defaultSchedulerRoundRobinMaxInterval)
case "POLLING_PARSING_ERROR_LIMIT":
p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
case "PROXY_IMAGES":
@@ -189,11 +191,15 @@ func (p *Parser) parseLines(lines []string) (err error) {
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)
if _, err := rand.Read(randomKey); err != nil {
return fmt.Errorf("config: unable to generate random key: %w", err)
}
p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey)
case "MEDIA_PROXY_PRIVATE_KEY":
randomKey := make([]byte, 16)
rand.Read(randomKey)
if _, err := rand.Read(randomKey); err != nil {
return fmt.Errorf("config: unable to generate random key: %w", err)
}
p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey)
case "MEDIA_PROXY_CUSTOM_URL":
p.opts.mediaProxyCustomURL = parseString(value, defaultMediaProxyURL)
@@ -236,7 +242,12 @@ func (p *Parser) parseLines(lines []string) (err error) {
case "HTTP_CLIENT_MAX_BODY_SIZE":
p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024)
case "HTTP_CLIENT_PROXY":
p.opts.httpClientProxy = parseString(value, defaultHTTPClientProxy)
p.opts.httpClientProxyURL, err = url.Parse(parseString(value, defaultHTTPClientProxy))
if err != nil {
return fmt.Errorf("config: invalid HTTP_CLIENT_PROXY value: %w", err)
}
case "HTTP_CLIENT_PROXIES":
p.opts.httpClientProxies = parseStringList(value, []string{})
case "HTTP_CLIENT_USER_AGENT":
p.opts.httpClientUserAgent = parseString(value, defaultHTTPClientUserAgent)
case "HTTP_SERVER_TIMEOUT":
+58
View File
@@ -5,6 +5,8 @@ package database // import "miniflux.app/v2/internal/database"
import (
"database/sql"
"miniflux.app/v2/internal/crypto"
)
var schemaVersion = len(migrations)
@@ -1008,4 +1010,60 @@ var migrations = []func(tx *sql.Tx, driver string) error{
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE feeds ADD COLUMN ntfy_topic text default '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
sql := `
ALTER TABLE icons ADD COLUMN external_id text default '';
CREATE UNIQUE INDEX icons_external_id_idx ON icons USING btree(external_id) WHERE external_id <> '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`
DECLARE id_cursor CURSOR FOR
SELECT
id
FROM icons
WHERE external_id = ''
FOR UPDATE`)
if err != nil {
return err
}
defer tx.Exec("CLOSE id_cursor")
for {
var id int64
if err := tx.QueryRow(`FETCH NEXT FROM id_cursor`).Scan(&id); err != nil {
if err == sql.ErrNoRows {
break
}
return err
}
_, err = tx.Exec(
`
UPDATE icons SET external_id = $1 WHERE id = $2
`,
crypto.GenerateRandomStringHex(20), id)
if err != nil {
return err
}
}
return nil
},
func(tx *sql.Tx, _ string) (err error) {
_, err = tx.Exec(`ALTER TABLE feeds ADD COLUMN proxy_url text default ''`)
return err
},
}
+31 -18
View File
@@ -20,6 +20,7 @@ import (
"miniflux.app/v2/internal/integration"
"miniflux.app/v2/internal/mediaproxy"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
mff "miniflux.app/v2/internal/reader/handler"
mfs "miniflux.app/v2/internal/reader/subscription"
@@ -683,7 +684,7 @@ func (h *handler) quickAddHandler(w http.ResponseWriter, r *http.Request) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
var rssBridgeURL string
if intg, err := h.store.Integration(userID); err == nil && intg != nil && intg.RSSBridgeEnabled {
@@ -735,17 +736,16 @@ func getFeed(stream Stream, store *storage.Storage, userID int64) (*model.Feed,
return store.FeedByID(userID, feedID)
}
func getOrCreateCategory(category Stream, store *storage.Storage, userID int64) (*model.Category, error) {
func getOrCreateCategory(streamCategory Stream, store *storage.Storage, userID int64) (*model.Category, error) {
switch {
case category.ID == "":
case streamCategory.ID == "":
return store.FirstCategory(userID)
case store.CategoryTitleExists(userID, category.ID):
return store.CategoryByTitle(userID, category.ID)
case store.CategoryTitleExists(userID, streamCategory.ID):
return store.CategoryByTitle(userID, streamCategory.ID)
default:
catRequest := model.CategoryRequest{
Title: category.ID,
}
return store.CreateCategory(userID, &catRequest)
return store.CreateCategory(userID, &model.CategoryCreationRequest{
Title: streamCategory.ID,
})
}
}
@@ -827,6 +827,14 @@ func move(stream Stream, destination Stream, store *storage.Storage, userID int6
return store.UpdateFeed(feed)
}
func (h *handler) feedIconURL(f *model.Feed) string {
if f.Icon != nil && f.Icon.ExternalIconID != "" {
return config.Opts.RootURL() + route.Path(h.router, "feedIcon", "externalIconID", f.Icon.ExternalIconID)
} else {
return ""
}
}
func (h *handler) editSubscriptionHandler(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
clientIP := request.ClientIP(r)
@@ -950,6 +958,7 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque
)
builder := h.store.NewEntryQueryBuilder(userID)
builder.WithEnclosures()
builder.WithoutStatus(model.EntryStatusRemoved)
builder.WithEntryIDs(itemIDs)
builder.WithSorting(model.DefaultSortingOrder, requestModifiers.SortDirection)
@@ -1134,20 +1143,23 @@ func (h *handler) renameTagHandler(w http.ResponseWriter, r *http.Request) {
json.NotFound(w, r)
return
}
categoryRequest := model.CategoryRequest{
Title: destination.ID,
categoryModificationRequest := model.CategoryModificationRequest{
Title: model.SetOptionalField(destination.ID),
}
verr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryRequest)
if verr != nil {
json.BadRequest(w, r, verr.Error())
if validationError := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryModificationRequest); validationError != nil {
json.BadRequest(w, r, validationError.Error())
return
}
categoryRequest.Patch(category)
err = h.store.UpdateCategory(category)
if err != nil {
categoryModificationRequest.Patch(category)
if err := h.store.UpdateCategory(category); err != nil {
json.ServerError(w, r, err)
return
}
OK(w, r)
}
@@ -1207,6 +1219,7 @@ func (h *handler) subscriptionListHandler(w http.ResponseWriter, r *http.Request
json.ServerError(w, r, err)
return
}
result.Subscriptions = make([]subscription, 0)
for _, feed := range feeds {
result.Subscriptions = append(result.Subscriptions, subscription{
@@ -1215,7 +1228,7 @@ func (h *handler) subscriptionListHandler(w http.ResponseWriter, r *http.Request
URL: feed.FeedURL,
Categories: []subscriptionCategory{{fmt.Sprintf(UserLabelPrefix, userID) + feed.Category.Title, feed.Category.Title, "folder"}},
HTMLURL: feed.SiteURL,
IconURL: "", // TODO: Icons are base64 encoded in the DB.
IconURL: h.feedIconURL(feed),
})
}
json.OK(w, r, result)
+2 -2
View File
@@ -37,7 +37,7 @@ func ServerError(w http.ResponseWriter, r *http.Request, err error) {
builder := response.New(w, r)
builder.WithStatus(http.StatusInternalServerError)
builder.WithHeader("Content-Security-Policy", `default-src 'self'`)
builder.WithHeader("Content-Security-Policy", response.ContentSecurityPolicyForUntrustedContent)
builder.WithHeader("Content-Type", "text/html; charset=utf-8")
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
builder.WithBody(err)
@@ -61,7 +61,7 @@ func BadRequest(w http.ResponseWriter, r *http.Request, err error) {
builder := response.New(w, r)
builder.WithStatus(http.StatusBadRequest)
builder.WithHeader("Content-Security-Policy", `default-src 'self'`)
builder.WithHeader("Content-Security-Policy", response.ContentSecurityPolicyForUntrustedContent)
builder.WithHeader("Content-Type", "text/html; charset=utf-8")
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
builder.WithBody(err)
+14
View File
@@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package response // import "miniflux.app/v2/internal/http/response"
// ContentSecurityPolicyForUntrustedContent is the default CSP for untrusted content.
// default-src 'none' disables all content sources
// form-action 'none' disables all form submissions
// sandbox enables a sandbox for the requested resource
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/form-action
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src
const ContentSecurityPolicyForUntrustedContent = `default-src 'none'; form-action 'none'; sandbox;`
+6 -1
View File
@@ -502,15 +502,20 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
}
if userIntegrations.NtfyEnabled && feed.NtfyEnabled {
ntfyTopic := feed.NtfyTopic
if ntfyTopic == "" {
ntfyTopic = userIntegrations.NtfyTopic
}
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),
slog.String("topic", ntfyTopic),
)
client := ntfy.NewClient(
userIntegrations.NtfyURL,
userIntegrations.NtfyTopic,
ntfyTopic,
userIntegrations.NtfyAPIToken,
userIntegrations.NtfyUsername,
userIntegrations.NtfyPassword,
@@ -5,8 +5,12 @@ package telegrambot // import "miniflux.app/v2/internal/integration/telegrambot"
import (
"fmt"
"log/slog"
"strconv"
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/urllib"
)
func PushEntry(feed *model.Feed, entry *model.Entry, botToken, chatID string, topicID *int64, disableWebPagePreview, disableNotification bool, disableButtons bool) error {
@@ -32,8 +36,16 @@ func PushEntry(feed *model.Feed, entry *model.Entry, botToken, chatID string, to
if !disableButtons {
var markupRow []*InlineKeyboardButton
websiteURLButton := InlineKeyboardButton{Text: "Go to website", URL: feed.SiteURL}
markupRow = append(markupRow, &websiteURLButton)
baseURL := config.Opts.BaseURL()
entryPath := "/unread/entry/" + strconv.FormatInt(entry.ID, 10)
minifluxEntryURL, err := urllib.JoinBaseURLAndPath(baseURL, entryPath)
if err != nil {
slog.Error("Unable to create Miniflux entry URL", slog.Any("error", err))
} else {
minifluxEntryURLButton := InlineKeyboardButton{Text: "Go to Miniflux", URL: minifluxEntryURL}
markupRow = append(markupRow, &minifluxEntryURLButton)
}
articleURLButton := InlineKeyboardButton{Text: "Go to article", URL: entry.URL}
markupRow = append(markupRow, &articleURLButton)
+14 -13
View File
@@ -83,7 +83,7 @@ func TestMissingTranslations(t *testing.T) {
for key := range references {
if _, found := messages[key]; !found {
t.Fatalf(`Translation key %q not found in language %q`, key, language)
t.Errorf(`Translation key %q not found in language %q`, key, language)
}
}
}
@@ -91,25 +91,26 @@ func TestMissingTranslations(t *testing.T) {
func TestTranslationFilePluralForms(t *testing.T) {
var numberOfPluralFormsPerLanguage = map[string]int{
"de_DE": 2,
"el_EL": 2,
"en_US": 2,
"es_ES": 2,
"fi_FI": 2,
"fr_FR": 2,
"de_DE": 2,
"pl_PL": 3,
"pt_BR": 2,
"zh_CN": 1,
"zh_TW": 1,
"nan_Latn_pehoeji": 1,
"nl_NL": 2,
"ru_RU": 3,
"hi_IN": 2,
"id_ID": 1,
"it_IT": 2,
"ja_JP": 1,
"nan_Latn_pehoeji": 1,
"nl_NL": 2,
"pl_PL": 3,
"pt_BR": 2,
"ro_RO": 3,
"ru_RU": 3,
"tr_TR": 2,
"el_EL": 2,
"fi_FI": 2,
"hi_IN": 2,
"uk_UA": 3,
"id_ID": 1,
"zh_CN": 1,
"zh_TW": 1,
}
for language := range AvailableLanguages {
messages, err := loadTranslationFile(language)
+13 -12
View File
@@ -5,23 +5,24 @@ package locale // import "miniflux.app/v2/internal/locale"
// AvailableLanguages is the list of available languages.
var AvailableLanguages = map[string]string{
"de_DE": "Deutsch",
"el_EL": "Ελληνικά",
"en_US": "English",
"es_ES": "Español",
"fi_FI": "Suomi",
"fr_FR": "Français",
"de_DE": "Deutsch",
"pl_PL": "Polski",
"pt_BR": "Português Brasileiro",
"zh_CN": "简体中文",
"zh_TW": "繁體中文",
"nan_Latn_pehoeji": "Pe̍h-ōe-jī",
"nl_NL": "Nederlands",
"ru_RU": "Русский",
"hi_IN": "हिन्दी",
"id_ID": "Bahasa Indonesia",
"it_IT": "Italiano",
"ja_JP": "日本語",
"nan_Latn_pehoeji": "Pe̍h-ōe-jī",
"nl_NL": "Nederlands",
"pl_PL": "Polski",
"pt_BR": "Português Brasileiro",
"ro_RO": "Română",
"ru_RU": "Русский",
"tr_TR": "Türkçe",
"el_EL": "Ελληνικά",
"fi_FI": "Suomi",
"hi_IN": "हिन्दी",
"uk_UA": "Українська",
"id_ID": "Bahasa Indonesia",
"zh_CN": "简体中文",
"zh_TW": "繁體中文",
}
+10
View File
@@ -71,6 +71,16 @@ var pluralForms = map[string]func(n int) int{
}
return 0
},
// nplurals=3; plural=(n==1 ? 0 : n==0 || (n%100 > 0 && n%100 < 20) ? 1 : 2);
"ro_RO": func(n int) int {
switch {
case n == 1:
return 0
case n == 0 || (n%100 > 0 && n%100 < 20):
return 1
}
return 2
},
"ru_RU": pluralFormRuSrUa,
// nplurals=2; plural=(n > 1);
"tr_TR": func(n int) int {
+5
View File
@@ -49,6 +49,11 @@ func TestPluralRules(t *testing.T) {
2: 1,
5: 1,
},
"ro_RO": {
1: 0,
2: 1,
5: 1,
},
"ru_RU": {
1: 0,
2: 1,
+21 -16
View File
@@ -71,11 +71,11 @@
"entry.shared_entry.title": "Öffnen Sie den öffentlichen Link",
"entry.state.loading": "Lade...",
"entry.state.saving": "Speichern...",
"entry.status.read": "Gelesen",
"entry.status.mark_as_read": "Als gelesen markieren",
"entry.status.mark_as_unread": "Als ungelesen markieren",
"entry.status.title": "Status des Artikels ändern",
"entry.status.toast.read": "Als gelesen markiert",
"entry.status.toast.unread": "Als ungelesen markiert",
"entry.status.unread": "Ungelesen",
"entry.tags.label": "Stichworte:",
"entry.unshare.label": "Nicht teilen",
"error.api_key_already_exists": "Dieser API-Schlüssel ist bereits vorhanden.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Ungültige Standard-Startseite!",
"error.invalid_display_mode": "Progressive-Web-App- (PWA-)Anzeigemodus",
"error.invalid_entry_direction": "Ungültige Sortierreihenfolge.",
"error.invalid_feed_proxy_url": "Ungültige Proxy-URL.",
"error.invalid_feed_url": "Ungültiger Feed-URL.",
"error.invalid_gesture_nav": "Ungültige Gestennavigation.",
"error.invalid_language": "Ungültige Sprache.",
@@ -128,6 +129,7 @@
"error.password_min_length": "Wenigstens 6 Zeichen müssen genutzt werden.",
"error.pocket_access_token": "Zugriffstoken konnte nicht von Pocket abgerufen werden!",
"error.pocket_request_token": "Anfrage-Token konnte nicht von Pocket abgerufen werden!",
"error.proxy_url_not_empty": "Die Proxy-URL darf nicht leer sein.",
"error.settings_block_rule_fieldname_invalid": "Ungültige Blockierregel: Regel #%d hat keinen gültigen Feldnamen (Optionen: %s)",
"error.settings_block_rule_invalid_regex": "Ungültige Blockierregel: Das Muster für Regel #%d ist kein zulässiger regulärer Ausdruck",
"error.settings_block_rule_regex_required": "Ungültige Blockierregel: Regel #%d hat kein Muster",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Passwort des Abonnements",
"form.feed.label.feed_url": "URL des Abonnements",
"form.feed.label.feed_username": "Benutzername des Abonnements",
"form.feed.label.fetch_via_proxy": "Über Proxy abrufen",
"form.feed.label.fetch_via_proxy": "Den auf Anwendungsebene konfigurierten Proxy verwenden",
"form.feed.label.hide_globally": "Einträge in der globalen Ungelesen-Liste ausblenden",
"form.feed.label.ignore_http_cache": "Ignoriere HTTP-Cache",
"form.feed.label.keeplist_rules": "Erlaubnisregeln",
@@ -186,13 +188,15 @@
"form.feed.label.ntfy_max_priority": "Höchste Ntfy-Priorität",
"form.feed.label.ntfy_min_priority": "Niedrigste Ntfy-Priorität",
"form.feed.label.ntfy_priority": "Ntfy-Priorität",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
"form.feed.label.pushover_low_priority": "Pushover low priority",
"form.feed.label.pushover_max_priority": "Pushover max priority",
"form.feed.label.pushover_min_priority": "Pushover min priority",
"form.feed.label.pushover_priority": "Pushover message priority",
"form.feed.label.ntfy_topic": "Ntfy-Thema (optional)",
"form.feed.label.proxy_url": "Proxy-URL",
"form.feed.label.pushover_activate": "Einträge an pushover.net senden",
"form.feed.label.pushover_default_priority": "Pushover-Standardpriorität",
"form.feed.label.pushover_high_priority": "Hohe Pushoverpriorität",
"form.feed.label.pushover_low_priority": "Niedrige Pushoverpriorität",
"form.feed.label.pushover_max_priority": "Höchste Pushoverpriorität",
"form.feed.label.pushover_min_priority": "Niedrigste Pushoverpriorität",
"form.feed.label.pushover_priority": "Pushover-Nachrichtenpriorität",
"form.feed.label.rewrite_rules": "Umschreiberegeln",
"form.feed.label.scraper_rules": "Extraktionsregeln",
"form.feed.label.site_url": "URL der Webseite",
@@ -254,7 +258,7 @@
"form.integration.ntfy_icon_url": "Ntfy-Symbol-URL (optional)",
"form.integration.ntfy_internal_links": "Interne Links beim Klicken verwenden (optional)",
"form.integration.ntfy_password": "Ntfy-Passwort (optional)",
"form.integration.ntfy_topic": "Ntfy-Thema",
"form.integration.ntfy_topic": "Ntfy-Thema (Standard, wenn nicht im Feed eingestellt)",
"form.integration.ntfy_url": "Ntfy-URL (optional, Standard ist ntfy.sh)",
"form.integration.ntfy_username": "Ntfy-Benutzername (optional)",
"form.integration.nunux_keeper_activate": "Artikel in Nunux Keeper speichern",
@@ -271,11 +275,11 @@
"form.integration.pocket_activate": "Einträge in Pocket speichern",
"form.integration.pocket_connect_link": "Verbinden Sie Ihr Pocket-Konto",
"form.integration.pocket_consumer_key": "Pocket-Verbraucher-Schlüssel",
"form.integration.pushover_activate": "Push entries to Pushover",
"form.integration.pushover_device": "Pushover device (optional)",
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
"form.integration.pushover_token": "Pushover application API token",
"form.integration.pushover_user": "Pushover user key",
"form.integration.pushover_activate": "Einträge an Pushover senden",
"form.integration.pushover_device": "Pushovergerät (optional)",
"form.integration.pushover_prefix": "Pushover-URL-Präfix (optional)",
"form.integration.pushover_token": "Pushover-Anwendungs-API-Token",
"form.integration.pushover_user": "Pushover-Benutzerschlüssel",
"form.integration.raindrop_activate": "Einträge in Raindrop speichern",
"form.integration.raindrop_collection_id": "Sammlungs-ID",
"form.integration.raindrop_tags": "Tags (kommagetrennt)",
@@ -400,6 +404,7 @@
"page.about.author": "Autor:",
"page.about.build_date": "Datum der Kompilierung:",
"page.about.credits": "Urheberrechte",
"page.about.db_usage": "Datenbankgröße:",
"page.about.global_config_options": "Globale Konfigurationsoptionen",
"page.about.go_version": "Go-Version:",
"page.about.license": "Lizenz:",
+9 -4
View File
@@ -71,11 +71,11 @@
"entry.shared_entry.title": "Ανοίξτε τον δημόσιο σύνδεσμο",
"entry.state.loading": "Φόρτωση...",
"entry.state.saving": "Aποθήκευση...",
"entry.status.read": "Αναγνωσμένο",
"entry.status.mark_as_read": "Επισήμανση ως αναγνωσμένο",
"entry.status.mark_as_unread": "Επισήμανση ως μη αναγνωσμένο",
"entry.status.title": "Αλλαγή κατάστασης καταχώρησης",
"entry.status.toast.read": "Επισήμανση ως αναγνωσμένο",
"entry.status.toast.unread": "Επισήμανση ως μη αναγνωσμένο",
"entry.status.unread": "Μη αναγνωσμένο",
"entry.tags.label": "Ετικέτες:",
"entry.unshare.label": "Aναίρεση Διαμοιρασμού",
"error.api_key_already_exists": "Αυτό το κλειδί API υπάρχει ήδη.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Μη έγκυρη προεπιλεγμένη αρχική σελίδα!",
"error.invalid_display_mode": "Μη έγκυρη λειτουργία εμφάνισης εφαρμογών ιστού.",
"error.invalid_entry_direction": "Μη έγκυρη κατεύθυνση ταξινόμησης άρθρων.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Μη έγκυρη διεύθυνση URL ροής.",
"error.invalid_gesture_nav": "Μη έγκυρη πλοήγηση με χειρονομίες.",
"error.invalid_language": "Μη έγκυρη γλώσσα.",
@@ -128,6 +129,7 @@
"error.password_min_length": "Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον 6 χαρακτήρες.",
"error.pocket_access_token": "Δεν είναι δυνατή η λήψη του access token από το Pocket!",
"error.pocket_request_token": "Δεν είναι δυνατή η λήψη του request token από το Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Κωδικός Πρόσβασης ροής",
"form.feed.label.feed_url": "Διεύθυνση URL ροής",
"form.feed.label.feed_username": "Όνομα Χρήστη ροής",
"form.feed.label.fetch_via_proxy": "Λήψη μέσω διακομιστή μεσολάβησης",
"form.feed.label.fetch_via_proxy": "Χρησιμοποιήστε τον διακομιστή μεσολάβησης που έχει ρυθμιστεί σε επίπεδο εφαρμογής",
"form.feed.label.hide_globally": "Απόκρυψη καταχωρήσεων σε γενική λίστα μη αναγνωσμένων",
"form.feed.label.ignore_http_cache": "Αγνοήστε την προσωρινή μνήμη HTTP",
"form.feed.label.keeplist_rules": "Κρατήστε Κανόνες",
@@ -186,6 +188,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -254,7 +258,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (default used if not set in feed)",
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.nunux_keeper_activate": "Αποθήκευση άρθρων στο Nunux Keeper",
@@ -400,6 +404,7 @@
"page.about.author": "Συγγραφέας:",
"page.about.build_date": "Ημερομηνία Κατασκευής:",
"page.about.credits": "Συνεισφέροντες",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "Γενικές ρυθμίσεις",
"page.about.go_version": "Έκδοση Go:",
"page.about.license": "Άδεια:",
+9 -4
View File
@@ -71,11 +71,11 @@
"entry.shared_entry.title": "Open the public link",
"entry.state.loading": "Loading…",
"entry.state.saving": "Saving…",
"entry.status.read": "Read",
"entry.status.mark_as_read": "Mark as read",
"entry.status.mark_as_unread": "Mark as unread",
"entry.status.title": "Change entry status",
"entry.status.toast.read": "Marked as read",
"entry.status.toast.unread": "Marked as unread",
"entry.status.unread": "Unread",
"entry.tags.label": "Tags:",
"entry.unshare.label": "Unshare",
"error.api_key_already_exists": "This API Key already exists.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Invalid default homepage!",
"error.invalid_display_mode": "Invalid web app display mode.",
"error.invalid_entry_direction": "Invalid entry direction.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Invalid feed URL.",
"error.invalid_gesture_nav": "Invalid gesture navigation.",
"error.invalid_language": "Invalid language.",
@@ -128,6 +129,7 @@
"error.password_min_length": "The password must have at least 6 characters.",
"error.pocket_access_token": "Unable to fetch access token from Pocket!",
"error.pocket_request_token": "Unable to fetch request token from Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Feed Password",
"form.feed.label.feed_url": "Feed URL",
"form.feed.label.feed_username": "Feed Username",
"form.feed.label.fetch_via_proxy": "Fetch via proxy",
"form.feed.label.fetch_via_proxy": "Use the proxy configured at the application level",
"form.feed.label.hide_globally": "Hide entries in global unread list",
"form.feed.label.ignore_http_cache": "Ignore HTTP cache",
"form.feed.label.keeplist_rules": "Keep Rules",
@@ -186,6 +188,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to Pushover",
"form.feed.label.pushover_default_priority": "Default priority",
"form.feed.label.pushover_high_priority": "High priority",
@@ -254,7 +258,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (default used if not set in feed)",
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.nunux_keeper_activate": "Save entries to Nunux Keeper",
@@ -400,6 +404,7 @@
"page.about.author": "Author:",
"page.about.build_date": "Build Date:",
"page.about.credits": "Credits",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "Global configuration options",
"page.about.go_version": "Go version:",
"page.about.license": "License:",
+9 -4
View File
@@ -71,11 +71,11 @@
"entry.shared_entry.title": "Abrir el enlace público",
"entry.state.loading": "Cargando...",
"entry.state.saving": "Guardando...",
"entry.status.read": "Leído",
"entry.status.mark_as_read": "Marcar como leído",
"entry.status.mark_as_unread": "Marcar como no leído",
"entry.status.title": "Cambiar estado del artículo",
"entry.status.toast.read": "Marcado como leído",
"entry.status.toast.unread": "Marcado como no leído",
"entry.status.unread": "No leído",
"entry.tags.label": "Etiquetas:",
"entry.unshare.label": "No compartir",
"error.api_key_already_exists": "Esta clave API ya existe.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "¡Página de inicio por defecto no válida!",
"error.invalid_display_mode": "Modo de visualización de la aplicación web no válido.",
"error.invalid_entry_direction": "Dirección de artículo no válida.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "URL de feed no válida.",
"error.invalid_gesture_nav": "Navegación por gestos no válida.",
"error.invalid_language": "Idioma no válido.",
@@ -128,6 +129,7 @@
"error.password_min_length": "La contraseña debería tener al menos 6 caracteres.",
"error.pocket_access_token": "Incapaz de obtener un token de acceso de Pocket!",
"error.pocket_request_token": "Incapaz de obtener un token de solicitud de Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"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_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_block_rule_regex_required": "Regla de bloqueo no válida: no se ha proporcionado el patrón de la regla #%d",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Contraseña de la fuente",
"form.feed.label.feed_url": "URL de la fuente",
"form.feed.label.feed_username": "Nombre de usuario de la fuente",
"form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
"form.feed.label.fetch_via_proxy": "Usar el proxy configurado a nivel de la aplicación",
"form.feed.label.hide_globally": "Ocultar artículos en la lista global de no leídos",
"form.feed.label.ignore_http_cache": "Ignorar caché HTTP",
"form.feed.label.keeplist_rules": "Reglas de Filtrado (Permitir)",
@@ -186,6 +188,8 @@
"form.feed.label.ntfy_max_priority": "Prioridad máxima a Ntfy",
"form.feed.label.ntfy_min_priority": "Prioridad mínima a Ntfy",
"form.feed.label.ntfy_priority": "Prioridad Ntfy",
"form.feed.label.ntfy_topic": "Tema Ntfy (opcional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -254,7 +258,7 @@
"form.integration.ntfy_icon_url": "URL del icono de Ntfy (opcional)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Contraseña de Ntfy (opcional)",
"form.integration.ntfy_topic": "Tema Ntfy",
"form.integration.ntfy_topic": "Tema Ntfy (por defecto, si no se establece en el feed)",
"form.integration.ntfy_url": "URL de Ntfy (opcional, la predeterminada es ntfy.sh)",
"form.integration.ntfy_username": "Nombre de usuario de Ntfy (opcional)",
"form.integration.nunux_keeper_activate": "Enviar artículos a Nunux Keeper",
@@ -400,6 +404,7 @@
"page.about.author": "Autor:",
"page.about.build_date": "Fecha de compilación:",
"page.about.credits": "Créditos",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "Opciones de configuración global",
"page.about.go_version": "Go versión:",
"page.about.license": "Licencia:",
+9 -4
View File
@@ -71,11 +71,11 @@
"entry.shared_entry.title": "Avaa julkinen linkki",
"entry.state.loading": "Ladataan...",
"entry.state.saving": "Tallennetaan...",
"entry.status.read": "Luettu",
"entry.status.mark_as_read": "Merkitse luetuksi",
"entry.status.mark_as_unread": "Merkitse lukemattomaksi",
"entry.status.title": "Vaihda artikkelin tilaa",
"entry.status.toast.read": "Merkitty luetuksi",
"entry.status.toast.unread": "Merkitty lukemattomaksi",
"entry.status.unread": "Lukematon",
"entry.tags.label": "Tags:",
"entry.unshare.label": "Poista jako",
"error.api_key_already_exists": "API-avain on jo olemassa.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Väärä oletusarvoinen kotisivu!",
"error.invalid_display_mode": "Virheellinen verkkosovelluksen näyttötila.",
"error.invalid_entry_direction": "Invalid entry direction.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Virheellinen syötteen URL-osoite.",
"error.invalid_gesture_nav": "Virheellinen ele-navigointi.",
"error.invalid_language": "Virheellinen kieli.",
@@ -128,6 +129,7 @@
"error.password_min_length": "Salasanassa on oltava vähintään 6 merkkiä.",
"error.pocket_access_token": "Unable to fetch access token from Pocket!",
"error.pocket_request_token": "Unable to fetch request token from Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Syötteen salasana",
"form.feed.label.feed_url": "Syötteen URL-osoite",
"form.feed.label.feed_username": "Syötteen käyttäjätunnus",
"form.feed.label.fetch_via_proxy": "Nouda välityspalvelimen kautta",
"form.feed.label.fetch_via_proxy": "Käytä sovellustasolla määritettyä välityspalvelinta",
"form.feed.label.hide_globally": "Piilota artikkelit lukemattomien listassa",
"form.feed.label.ignore_http_cache": "Ohita HTTP-välimuisti",
"form.feed.label.keeplist_rules": "Keep-säännöt",
@@ -186,6 +188,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -254,7 +258,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (default used if not set in feed)",
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.nunux_keeper_activate": "Tallenna artikkelit Nunux Keeperiin",
@@ -400,6 +404,7 @@
"page.about.author": "Tekijä:",
"page.about.build_date": "Valmistuspäivä:",
"page.about.credits": "Kiitokset",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "Yleiset asetukset",
"page.about.go_version": "Go-versio:",
"page.about.license": "Lisenssi:",
+9 -4
View File
@@ -71,11 +71,11 @@
"entry.shared_entry.title": "Ouvrir le lien public",
"entry.state.loading": "Chargement...",
"entry.state.saving": "Sauvegarde en cours...",
"entry.status.read": "Lu",
"entry.status.mark_as_read": "Marquer comme lu",
"entry.status.mark_as_unread": "Marquer comme non lu",
"entry.status.title": "Changer le statut de l'entrée",
"entry.status.toast.read": "Marqué comme lu",
"entry.status.toast.unread": "Marqué comme non lu",
"entry.status.unread": "Non lu",
"entry.tags.label": "Libellés :",
"entry.unshare.label": "Enlever le partage",
"error.api_key_already_exists": "Cette clé d'API existe déjà.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Page d'accueil par défaut invalide !",
"error.invalid_display_mode": "Mode d'affichage de l'application web non valide.",
"error.invalid_entry_direction": "Ordre de trie non valide.",
"error.invalid_feed_proxy_url": "L'URL du proxy n'est pas valide.",
"error.invalid_feed_url": "URL de flux non valide.",
"error.invalid_gesture_nav": "Navigation gestuelle non valide.",
"error.invalid_language": "Langue non valide.",
@@ -128,6 +129,7 @@
"error.password_min_length": "Vous devez utiliser au moins 6 caractères pour le mot de passe.",
"error.pocket_access_token": "Impossible de récupérer le jeton d'accès depuis Pocket !",
"error.pocket_request_token": "Impossible de récupérer le jeton d'accès depuis Pocket !",
"error.proxy_url_not_empty": "L'URL du proxy ne peut pas être vide.",
"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_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_block_rule_regex_required": "Règle de blocage invalide : le motif de la règle n°%d n'est pas fourni",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Mot de passe du flux",
"form.feed.label.feed_url": "URL du flux",
"form.feed.label.feed_username": "Nom d'utilisateur du flux",
"form.feed.label.fetch_via_proxy": "Récupérer via proxy",
"form.feed.label.fetch_via_proxy": "Utiliser le proxy configuré au niveau de l'application",
"form.feed.label.hide_globally": "Masquer les entrées dans la liste globale non lue",
"form.feed.label.ignore_http_cache": "Ignorer le cache HTTP",
"form.feed.label.keeplist_rules": "Règles d'autorisation",
@@ -186,6 +188,8 @@
"form.feed.label.ntfy_max_priority": "Priorité maximale de notification",
"form.feed.label.ntfy_min_priority": "Priorité minimale de notification",
"form.feed.label.ntfy_priority": "Priorité de notification",
"form.feed.label.ntfy_topic": "Sujet Ntfy (facultatif)",
"form.feed.label.proxy_url": "URL du proxy",
"form.feed.label.pushover_activate": "Activer les notifications vers Pushover",
"form.feed.label.pushover_default_priority": "Priorité par défaut",
"form.feed.label.pushover_high_priority": "Priorité élevée",
@@ -254,7 +258,7 @@
"form.integration.ntfy_icon_url": "URL de l'icône Ntfy (facultatif)",
"form.integration.ntfy_internal_links": "Utiliser les liens internes vers Miniflux (facultatif)",
"form.integration.ntfy_password": "Mot de passe Ntfy (facultatif)",
"form.integration.ntfy_topic": "Sujet Ntfy",
"form.integration.ntfy_topic": "Sujet Ntfy (défaut s'il n'est pas défini dans le flux)",
"form.integration.ntfy_url": "URL de Ntfy (optionnel, ntfy.sh par défaut)",
"form.integration.ntfy_username": "Nom d'utilisateur Ntfy (optionnel)",
"form.integration.nunux_keeper_activate": "Sauvegarder les articles vers Nunux Keeper",
@@ -400,6 +404,7 @@
"page.about.author": "Auteur :",
"page.about.build_date": "Date de la compilation :",
"page.about.credits": "Crédits",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "Options de configuration globales",
"page.about.go_version": "Version de Go :",
"page.about.license": "Licence :",
+9 -4
View File
@@ -71,11 +71,11 @@
"entry.shared_entry.title": "सार्वजनिक लिंक खोले",
"entry.state.loading": "लोड हो रहा है...",
"entry.state.saving": "सहेजा जा रहा है...",
"entry.status.read": "पढ़े",
"entry.status.mark_as_read": "पढ़े हुए का चिह्न",
"entry.status.mark_as_unread": "अपठित के रूप में चिह्नित करें",
"entry.status.title": "प्रविष्टि स्थिति बदलें",
"entry.status.toast.read": "पढ़ा हुआ चिह्नित करे",
"entry.status.toast.unread": "अपठित के रूप में चिह्नित",
"entry.status.unread": "अपठित",
"entry.tags.label": "टैग:",
"entry.unshare.label": "न साझा कारें",
"error.api_key_already_exists": "यह एपीआई कुंजी पहले से मौजूद है।",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "अमान्य डिफ़ॉल्ट मुखपृष्ठ!",
"error.invalid_display_mode": "अमान्य वेब ऐप्लिकेशन प्रदर्शन मोड.",
"error.invalid_entry_direction": "अमान्य प्रवेश दिशा।",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "दृष्टिकोण यूआरएल.",
"error.invalid_gesture_nav": "अमान्य इशारा नेविगेशन।",
"error.invalid_language": "अमान्य भाषा.",
@@ -128,6 +129,7 @@
"error.password_min_length": "पासवर्ड में कम से कम 6 अक्षर होने चाहिए।",
"error.pocket_access_token": "पॉकेट से एक्सेस टोकन प्राप्त करने में असमर्थ!",
"error.pocket_request_token": "पॉकेट से अनुरोध टोकन लाने में असमर्थ!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "फ़ीड पासवर्ड",
"form.feed.label.feed_url": "फ़ीड यूआरएल",
"form.feed.label.feed_username": "फ़ीड उपयोगकर्ता नाम",
"form.feed.label.fetch_via_proxy": "प्रॉक्सी के माध्यम से प्राप्त करें",
"form.feed.label.fetch_via_proxy": "प्लिकेशन स्तर पर कॉन्फ़िगर किए गए प्रॉक्सी का उपयोग करें",
"form.feed.label.hide_globally": "वैश्विक अपठित सूची में प्रविष्टियां छिपाएं",
"form.feed.label.ignore_http_cache": "एचटीटीपी कैश पर ध्यान न दें",
"form.feed.label.keeplist_rules": "नियम बनाए रखें",
@@ -186,6 +188,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -254,7 +258,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (default used if not set in feed)",
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.nunux_keeper_activate": "विषय-वस्तु को ननक्स कीपर में सहेजें",
@@ -400,6 +404,7 @@
"page.about.author": "रचयिता:",
"page.about.build_date": "बनाने की तिथि:",
"page.about.credits": "आभार सूची",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "वैश्विक विन्यास विकल्प",
"page.about.go_version": "गो संस्करण:",
"page.about.license": "अनुज्ञा:",
+9 -4
View File
@@ -69,11 +69,11 @@
"entry.shared_entry.title": "Buka tautan publik",
"entry.state.loading": "Memuat...",
"entry.state.saving": "Menyimpan...",
"entry.status.read": "Telah dibaca",
"entry.status.mark_as_read": "Telah dibaca",
"entry.status.mark_as_unread": "Belum dibaca",
"entry.status.title": "Ubah status entri",
"entry.status.toast.read": "Ditandai sebagai telah dibaca",
"entry.status.toast.unread": "Ditandai sebagai belum dibaca",
"entry.status.unread": "Belum dibaca",
"entry.tags.label": "Tanda:",
"entry.unshare.label": "Batal bagikan",
"error.api_key_already_exists": "Kunci API ini sudah ada.",
@@ -115,6 +115,7 @@
"error.invalid_default_home_page": "Beranda baku tidak valid!",
"error.invalid_display_mode": "Mode tampilan aplikasi web tidak valid.",
"error.invalid_entry_direction": "Urutan entri tidak valid.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "URL umpan tidak valid.",
"error.invalid_gesture_nav": "Navigasi gestur tidak valid.",
"error.invalid_language": "Bahasa tidak valid.",
@@ -126,6 +127,7 @@
"error.password_min_length": "Kata sandi harus memiliki setidaknya 6 karakter.",
"error.pocket_access_token": "Tidak bisa mendapatkan token akses dari Pocket!",
"error.pocket_request_token": "Tidak bisa mendapatkan token permintaan dari Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -172,7 +174,7 @@
"form.feed.label.feed_password": "Kata Sandi Umpan",
"form.feed.label.feed_url": "URL Umpan",
"form.feed.label.feed_username": "Nama Pengguna Umpan",
"form.feed.label.fetch_via_proxy": "Ambil via Proksi",
"form.feed.label.fetch_via_proxy": "Gunakan proxy yang dikonfigurasi di tingkat aplikasi",
"form.feed.label.hide_globally": "Sembunyikan entri di daftar belum dibaca global",
"form.feed.label.ignore_http_cache": "Abaikan Tembolok HTTP",
"form.feed.label.keeplist_rules": "Aturan Simpan",
@@ -184,6 +186,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -252,7 +256,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (default used if not set in feed)",
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.nunux_keeper_activate": "Simpan artikel ke Nunux Keeper",
@@ -398,6 +402,7 @@
"page.about.author": "Pengembang:",
"page.about.build_date": "Tanggal Penyusunan:",
"page.about.credits": "Pengembang",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "Pengaturan Konfigurasi Global",
"page.about.go_version": "Versi Go:",
"page.about.license": "Lisensi:",
+9 -4
View File
@@ -71,11 +71,11 @@
"entry.shared_entry.title": "Apri il link pubblico",
"entry.state.loading": "Caricamento in corso...",
"entry.state.saving": "Salvataggio in corso...",
"entry.status.read": "Letto",
"entry.status.mark_as_read": "Segna come letto",
"entry.status.mark_as_unread": "Segna come non letto",
"entry.status.title": "Cambia lo stato dell'articolo",
"entry.status.toast.read": "Contrassegnato come letto",
"entry.status.toast.unread": "Contrassegnato come non letto",
"entry.status.unread": "Da leggere",
"entry.tags.label": "Tag:",
"entry.unshare.label": "Unshare",
"error.api_key_already_exists": "Questa chiave API esiste già.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Pagina iniziale predefinita non valida!",
"error.invalid_display_mode": "Modalità di visualizzazione web app non valida.",
"error.invalid_entry_direction": "Ordinamento non valido.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "URL del feed non valido.",
"error.invalid_gesture_nav": "Navigazione gestuale non valida.",
"error.invalid_language": "Lingua non valida.",
@@ -128,6 +129,7 @@
"error.password_min_length": "La password deve contenere almeno 6 caratteri.",
"error.pocket_access_token": "Non sono riuscito ad ottenere l'access token da Pocket!",
"error.pocket_request_token": "Non sono riuscito ad ottenere il request token da Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Password del feed",
"form.feed.label.feed_url": "URL del feed",
"form.feed.label.feed_username": "Nome utente del feed",
"form.feed.label.fetch_via_proxy": "Recuperare tramite proxy",
"form.feed.label.fetch_via_proxy": "Usa il proxy configurato a livello di applicazione",
"form.feed.label.hide_globally": "Nascondere le voci nella lista globale dei non letti",
"form.feed.label.ignore_http_cache": "Ignora cache HTTP",
"form.feed.label.keeplist_rules": "Regole di autorizzazione",
@@ -186,6 +188,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -254,7 +258,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (default used if not set in feed)",
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.nunux_keeper_activate": "Salva gli articoli su Nunux Keeper",
@@ -400,6 +404,7 @@
"page.about.author": "Autore:",
"page.about.build_date": "Data della build:",
"page.about.credits": "Crediti",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "Opzioni di configurazione globali",
"page.about.go_version": "Go versione:",
"page.about.license": "Licenza:",
+9 -4
View File
@@ -69,11 +69,11 @@
"entry.shared_entry.title": "公開リンクを開く",
"entry.state.loading": "読み込み中…",
"entry.state.saving": "保存中…",
"entry.status.read": "既読にする",
"entry.status.mark_as_read": "既読にする",
"entry.status.mark_as_unread": "未読に戻す",
"entry.status.title": "記事の状態を変更",
"entry.status.toast.read": "既読にしました",
"entry.status.toast.unread": "未読にしました",
"entry.status.unread": "未読にする",
"entry.tags.label": "タグ:",
"entry.unshare.label": "共有を解除",
"error.api_key_already_exists": "この API キーは既に存在します。",
@@ -115,6 +115,7 @@
"error.invalid_default_home_page": "デフォルトのトップページが無効です",
"error.invalid_display_mode": "Web アプリの表示モードが無効です。",
"error.invalid_entry_direction": "記事の表示順が無効です。",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "フィード URL が無効です。",
"error.invalid_gesture_nav": "ジェスチャー ナビゲーションが無効です。",
"error.invalid_language": "言語が無効です。",
@@ -126,6 +127,7 @@
"error.password_min_length": "パスワードは6文字以上である必要があります。",
"error.pocket_access_token": "Pocket の access token が取得できません!",
"error.pocket_request_token": "Pocket の request token が取得できません!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -172,7 +174,7 @@
"form.feed.label.feed_password": "フィードのパスワード",
"form.feed.label.feed_url": "フィード URL",
"form.feed.label.feed_username": "フィードのユーザー名",
"form.feed.label.fetch_via_proxy": "プロキシ経由で取得",
"form.feed.label.fetch_via_proxy": "アプリケーションレベルで設定されたプロキシを使用する",
"form.feed.label.hide_globally": "未読一覧に記事を表示しない",
"form.feed.label.ignore_http_cache": "HTTPキャッシュを無視",
"form.feed.label.keeplist_rules": "Keep ルール",
@@ -184,6 +186,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -252,7 +256,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (default used if not set in feed)",
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.nunux_keeper_activate": "Nunux Keeper に記事を保存する",
@@ -398,6 +402,7 @@
"page.about.author": "作者:",
"page.about.build_date": "ビルド日時:",
"page.about.credits": "著作権表示",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "グローバル構成オプション",
"page.about.go_version": "Go バージョン:",
"page.about.license": "ライセンス:",
@@ -69,11 +69,11 @@
"entry.shared_entry.title": "Phah khui kong-khai ê liân-kiat",
"entry.state.loading": "Tng leh chip-hêng…",
"entry.state.saving": "Tng leh pó-chûn…",
"entry.status.read": "Chù chòe tha̍k--kè",
"entry.status.mark_as_read": "Chù chòe tha̍k kè",
"entry.status.mark_as_unread": "Chù chòe ah-bōe tha̍k",
"entry.status.title": "Kái chōng-thài",
"entry.status.toast.read": "Chù chòe tha̍k kè chòe soah",
"entry.status.toast.unread": "Chù chòe ah-bōe tha̍k chòe soah",
"entry.status.unread": "Chù chòe ah-bōe tha̍k",
"entry.tags.label": "Khan-á:",
"entry.unshare.label": "Chhú-siau hun-hióng",
"error.api_key_already_exists": "Chit ê API só-sî í-keng chûn-chāi",
@@ -115,6 +115,7 @@
"error.invalid_default_home_page": "Ū-siat chú-ia̍h ū būn-tôe!",
"error.invalid_display_mode": "Ū būn-tôe ê su-li̍p bô͘-sek.",
"error.invalid_entry_direction": "Ū būn-tôe ê su-li̍p hong-hiòng.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Beh tēng ê siau-sit lâi-goân ê bāng-chí ū būn-tôe.",
"error.invalid_gesture_nav": "Chhiú-sè tō-lám ū būn-tôe.",
"error.invalid_language": "Ū būn-tôe ê gú-giân.",
@@ -126,6 +127,7 @@
"error.password_min_length": "Chhiáⁿ chì-chió ài su-li̍p la̍k ê lī goân.",
"error.pocket_access_token": "Bô-hoat-tō͘ ùi Pocket thê tio̍h access token",
"error.pocket_request_token": "Bô-hoat-tō͘ ùi Pocket thê tio̍h request token",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Bô-hāu ê hong-só kui-chek: kui-chek #%d khiàm ū-hāu ê lân-ūi miâ (e-sai ê soán-hāng: %s)",
"error.settings_block_rule_invalid_regex": "Bô-hāu ê hong-só kui-chek: kui-chek #%d ê bô͘-sek m̄ sī ha̍p-hoat ê chiàⁿ-kui piáu-ta̍t sek",
"error.settings_block_rule_regex_required": "Bô-hāu ê hong-só kui-chek: kui-chek #%d bô thê-kiong chiàⁿ-kui piáu-ta̍t sek",
@@ -172,7 +174,7 @@
"form.feed.label.feed_password": "Siau-sit lâi-goân bi̍t-bé",
"form.feed.label.feed_url": "Siau-sit lâi-goân bāng-chí",
"form.feed.label.feed_username": "Siau-sit lâi-goân kháu-chō miâ",
"form.feed.label.fetch_via_proxy": "Thàu-kè tāi-lí lia̍h",
"form.feed.label.fetch_via_proxy": "Iōng tī su-hāu-khì siat-tēng ê proxy",
"form.feed.label.hide_globally": "Tī choân-he̍k ah-bōe tha̍k--ê lia̍t-pió am-khàm siau-sit",
"form.feed.label.ignore_http_cache": "Pàng-ba̍k HTTP cache",
"form.feed.label.keeplist_rules": "Pó-liû kui-chek",
@@ -184,6 +186,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy siōng koân iu-sian sūn-sū",
"form.feed.label.ntfy_min_priority": "Ntfy siōng kē iu-sian sūn-sū",
"form.feed.label.ntfy_priority": "Ntfy iu-sian sūn-sū",
"form.feed.label.ntfy_topic": "Ntfy topic (soán thiⁿ)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to Pushover",
"form.feed.label.pushover_default_priority": "Default priority",
"form.feed.label.pushover_high_priority": "High priority",
@@ -252,7 +256,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon bāng-chí (soán thiⁿ)",
"form.integration.ntfy_internal_links": "Tiám ê sî-chūn iōng lāi-pō͘ liân-kiat (soán thiⁿ)",
"form.integration.ntfy_password": "Ntfy bi̍t-bé (soán thiⁿ)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (chhī-liāu nā bô siat-tēng, tiō iōng ī-siat-ti̍t)",
"form.integration.ntfy_url": "Ntfy bāng-chí (soán thiⁿ, ū-siat sī ntfy.sh)",
"form.integration.ntfy_username": "Ntfy kháu-chō miâ (soán thiⁿ)",
"form.integration.nunux_keeper_activate": "Pó-chûn siau-sit kàu Nunux Keeper",
@@ -398,6 +402,7 @@
"page.about.author": "Chok-chiá: ",
"page.about.build_date": "Kiàn-tì li̍t-kî:",
"page.about.credits": "Pán-koân",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "Choân-he̍k siat-tēng soán-hāng",
"page.about.go_version": "Go pán-pún:",
"page.about.license": "Pàng-koân:",
+9 -4
View File
@@ -71,11 +71,11 @@
"entry.shared_entry.title": "Open de openbare link",
"entry.state.loading": "Laden...",
"entry.state.saving": "Opslaan...",
"entry.status.read": "Gelezen",
"entry.status.mark_as_read": "Markeren als gelezen",
"entry.status.mark_as_unread": "Markeren als ongelezen",
"entry.status.title": "Verander artikelstatus",
"entry.status.toast.read": "Gemarkeerd als gelezen",
"entry.status.toast.unread": "Gemarkeerd als ongelezen",
"entry.status.unread": "Ongelezen",
"entry.tags.label": "Tags:",
"entry.unshare.label": "Delen ongedaan maken",
"error.api_key_already_exists": "Deze API-sleutel bestaat al.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Ongeldige startpagina!",
"error.invalid_display_mode": "Ongeldige weergavemodus voor de webapp.",
"error.invalid_entry_direction": "Ongeldige sorteervolgorde.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Ongeldige feed URL.",
"error.invalid_gesture_nav": "Ongeldige gebarennavigatie.",
"error.invalid_language": "Ongeldige taal.",
@@ -128,6 +129,7 @@
"error.password_min_length": "Minimaal 6 tekens gebruiken.",
"error.pocket_access_token": "Kon geen toegangstoken ophalen van Pocket!",
"error.pocket_request_token": "Kon geen aanvraagtoken ophalen van Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Ongeldige blokkeerregel: regel #%d mist een geldige veldnaam (Opties: %s)",
"error.settings_block_rule_invalid_regex": "Ongeldige blokkeerregel: het patroon van regel #%d is geen geldige regex",
"error.settings_block_rule_regex_required": "Ongeldige blokkeerregel: het patroon van regel #%d is niet opgegeven",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Feed wachtwoord",
"form.feed.label.feed_url": "Feed URL",
"form.feed.label.feed_username": "Feed gebruikersnaam",
"form.feed.label.fetch_via_proxy": "Ophalen via proxy",
"form.feed.label.fetch_via_proxy": "Gebruik de proxy die op applicatieniveau is geconfigureerd",
"form.feed.label.hide_globally": "Verberg artikelen in de globale ongelezen lijst",
"form.feed.label.ignore_http_cache": "Negeer HTTP-cache",
"form.feed.label.keeplist_rules": "Bewaarregels",
@@ -186,6 +188,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy maximale prioriteit",
"form.feed.label.ntfy_min_priority": "Ntfy minimale prioriteit",
"form.feed.label.ntfy_priority": "Ntfy prioriteit",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -254,7 +258,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optioneel)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy wachtwoord (optioneel)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (standaard gebruikt als deze niet is ingesteld in feed)",
"form.integration.ntfy_url": "Ntfy URL (optioneel, standaard is ntfy.sh)",
"form.integration.ntfy_username": "Ntfy gebruikersnaam (optioneel)",
"form.integration.nunux_keeper_activate": "Artikelen opslaan in Nunux Keeper",
@@ -400,6 +404,7 @@
"page.about.author": "Auteur:",
"page.about.build_date": "Compilatiedatum:",
"page.about.credits": "Credits",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "Globale Configuratie Opties",
"page.about.go_version": "Go versie:",
"page.about.license": "Licentie:",
+21 -16
View File
@@ -73,11 +73,11 @@
"entry.shared_entry.title": "Otwórz publiczne łącze",
"entry.state.loading": "Ładowanie…",
"entry.state.saving": "Zapisywanie…",
"entry.status.read": "Przeczytany",
"entry.status.mark_as_read": "Oznacz jako przeczytany",
"entry.status.mark_as_unread": "Oznacz jako nieprzeczytany",
"entry.status.title": "Zmień status wpisu",
"entry.status.toast.read": "Oznaczono jako przeczytany",
"entry.status.toast.unread": "Oznaczono jako nieprzeczytany",
"entry.status.unread": "Nieprzeczytany",
"entry.tags.label": "Znaczniki:",
"entry.unshare.label": "Cofnij udostępnianie",
"error.api_key_already_exists": "Ten klucz API już istnieje.",
@@ -119,6 +119,7 @@
"error.invalid_default_home_page": "Nieprawidłowa domyślna strona główna!",
"error.invalid_display_mode": "Nieprawidłowy tryb wyświetlania aplikacji sieciowej.",
"error.invalid_entry_direction": "Nieprawidłowa kolejność sortowania.",
"error.invalid_feed_proxy_url": "Nieprawidłowy adres URL serwera proxy.",
"error.invalid_feed_url": "Nieprawidłowy adres URL kanału.",
"error.invalid_gesture_nav": "Nieprawidłowa nawigacja gestami.",
"error.invalid_language": "Nieprawidłowy język.",
@@ -130,6 +131,7 @@
"error.password_min_length": "Musisz użyć co najmniej 6 znaków.",
"error.pocket_access_token": "Nie można pobrać tokena dostępu z Pocket!",
"error.pocket_request_token": "Nie można pobrać tokena żądania z Pocket!",
"error.proxy_url_not_empty": "Adres URL serwera proxy nie może być pusty.",
"error.settings_block_rule_fieldname_invalid": "Nieprawidłowa reguła blokowania: w regule #%d brakuje prawidłowej nazwy pola (opcje: %s)",
"error.settings_block_rule_invalid_regex": "Nieprawidłowa reguła blokowania: wzór reguły #%d nie jest prawidłowym wyrażeniem regularnym",
"error.settings_block_rule_regex_required": "Nieprawidłowa reguła blokowania: nie podano wzorca reguły #%d",
@@ -176,7 +178,7 @@
"form.feed.label.feed_password": "Hasło do subskrypcji",
"form.feed.label.feed_url": "Adres URL kanału",
"form.feed.label.feed_username": "Nazwa użytkownika subskrypcji",
"form.feed.label.fetch_via_proxy": "Pobierz przez proxy",
"form.feed.label.fetch_via_proxy": "Użyj serwera proxy skonfigurowanego na poziomie aplikacji",
"form.feed.label.hide_globally": "Ukryj wpisy na globalnej liście nieprzeczytanych",
"form.feed.label.ignore_http_cache": "Zignoruj pamięć podręczną HTTP",
"form.feed.label.keeplist_rules": "Reguły utrzymywania",
@@ -188,13 +190,15 @@
"form.feed.label.ntfy_max_priority": "Maksymalny priorytet ntfy",
"form.feed.label.ntfy_min_priority": "Minimalny priorytet ntfy",
"form.feed.label.ntfy_priority": "Priorytet ntfy",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
"form.feed.label.pushover_low_priority": "Pushover low priority",
"form.feed.label.pushover_max_priority": "Pushover max priority",
"form.feed.label.pushover_min_priority": "Pushover min priority",
"form.feed.label.pushover_priority": "Pushover message priority",
"form.feed.label.ntfy_topic": "Temat ntfy (opcjonalny)",
"form.feed.label.proxy_url": "Adres URL serwera proxy",
"form.feed.label.pushover_activate": "Prześlij wpisy do pushover.net",
"form.feed.label.pushover_default_priority": "Domyślny priorytet Pushover",
"form.feed.label.pushover_high_priority": "Wysoki priorytet Pushover",
"form.feed.label.pushover_low_priority": "Niski priorytet Pushover",
"form.feed.label.pushover_max_priority": "Maksymalny priorytet Pushover",
"form.feed.label.pushover_min_priority": "Minimalny priorytet Pushover",
"form.feed.label.pushover_priority": "Priorytet wiadomości Pushover",
"form.feed.label.rewrite_rules": "Reguły zapisu",
"form.feed.label.scraper_rules": "Reguły ekstrakcji",
"form.feed.label.site_url": "Adres URL strony",
@@ -256,7 +260,7 @@
"form.integration.ntfy_icon_url": "Adres URL ikony ntfy (opcjonalny)",
"form.integration.ntfy_internal_links": "Używaj łączy wewnętrznych po kliknięciu (opcjonalnie)",
"form.integration.ntfy_password": "Hasło do ntfy (opcjonalne)",
"form.integration.ntfy_topic": "Temay ntfy",
"form.integration.ntfy_topic": "Temat ntfy (domyślny, jeśli nie został ustawiony w kanale)",
"form.integration.ntfy_url": "Adres URL ntfy (opcjonalny, domyślny to ntfy.sh)",
"form.integration.ntfy_username": "Login do ntfy (opcjonalny)",
"form.integration.nunux_keeper_activate": "Zapisuj wpisy w Nunux Keeper",
@@ -273,11 +277,11 @@
"form.integration.pocket_activate": "Zapisuj wpisy w Pocket",
"form.integration.pocket_connect_link": "Połącz swoje konto Pocket",
"form.integration.pocket_consumer_key": "Klucz klienta do Pocket",
"form.integration.pushover_activate": "Push entries to Pushover",
"form.integration.pushover_device": "Pushover device (optional)",
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
"form.integration.pushover_token": "Pushover application API token",
"form.integration.pushover_user": "Pushover user key",
"form.integration.pushover_activate": "Prześlij wpisy do Pushover",
"form.integration.pushover_device": "Urządzenie Pushover (opcjonalne)",
"form.integration.pushover_prefix": "Prefiks adresu URL Pushover (opcjonalny)",
"form.integration.pushover_token": "Token API aplikacji Pushover",
"form.integration.pushover_user": "Klucz użytkownika Pushover",
"form.integration.raindrop_activate": "Zapisuj wpisy do Raindrop",
"form.integration.raindrop_collection_id": "Identyfikator kolekcji",
"form.integration.raindrop_tags": "Znaczniki (oddzielone przecinkami)",
@@ -402,6 +406,7 @@
"page.about.author": "Autor:",
"page.about.build_date": "Data opracowania:",
"page.about.credits": "Prawa autorskie",
"page.about.db_usage": "Rozmiar bazy danych:",
"page.about.global_config_options": "Globalne opcje konfiguracji",
"page.about.go_version": "Wersja Go:",
"page.about.license": "Licencja:",
+9 -4
View File
@@ -71,11 +71,11 @@
"entry.shared_entry.title": "Abrir link público",
"entry.state.loading": "Carregando...",
"entry.state.saving": "Salvando...",
"entry.status.read": "Lido",
"entry.status.mark_as_read": "Marcar como lido",
"entry.status.mark_as_unread": "Marcar como não lido",
"entry.status.title": "Modificar estado deste item",
"entry.status.toast.read": "Marcado como lido",
"entry.status.toast.unread": "Marcado como não lido",
"entry.status.unread": "Não lido",
"entry.tags.label": "Etiquetas:",
"entry.unshare.label": "Descompartilhar",
"error.api_key_already_exists": "Essa chave de API já existe.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Página inicial por defeito inválida!",
"error.invalid_display_mode": "Modo de exibição de aplicativo inválido da web.",
"error.invalid_entry_direction": "Direção de entrada inválida.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "URL de feed inválido.",
"error.invalid_gesture_nav": "Navegação por gestos inválida.",
"error.invalid_language": "Idioma inválido.",
@@ -128,6 +129,7 @@
"error.password_min_length": "A senha deve ter no mínimo 6 caracteres.",
"error.pocket_access_token": "Não foi possível obter um token de acesso no Pocket!",
"error.pocket_request_token": "Não foi possível obter um pedido de token no Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Senha da fonte",
"form.feed.label.feed_url": "URL da fonte",
"form.feed.label.feed_username": "Nome de usuário da fonte",
"form.feed.label.fetch_via_proxy": "Buscar via proxy",
"form.feed.label.fetch_via_proxy": "Usar o proxy configurado no nível da aplicação",
"form.feed.label.hide_globally": "Ocultar entradas na lista global não lida",
"form.feed.label.ignore_http_cache": "Ignorar cache HTTP",
"form.feed.label.keeplist_rules": "Regras de permissão",
@@ -186,6 +188,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -254,7 +258,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (default if not set in feed)",
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.nunux_keeper_activate": "Salvar itens no Nunux Keeper",
@@ -400,6 +404,7 @@
"page.about.author": "Autor:",
"page.about.build_date": "Compilado em:",
"page.about.credits": "Créditos",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "opções de configuração global",
"page.about.go_version": "Go versão:",
"page.about.license": "Licença:",
+626
View File
@@ -0,0 +1,626 @@
{
"action.cancel": "abandon",
"action.download": "Descărcare",
"action.edit": "Editare",
"action.home_screen": "Adaugă pe ecranul principal",
"action.import": "Importă",
"action.login": "Autentificare",
"action.or": "sau",
"action.remove": "Elimină",
"action.remove_feed": "Elimină acest flux",
"action.save": "Salvează",
"action.subscribe": "Abonează-te",
"action.update": "Actualizare",
"alert.account_linked": "Contul dvs. extern este atașat!",
"alert.account_unlinked": "Am decuplat contul dvs. extern!",
"alert.background_feed_refresh": "Toate fluxurile sunt actualizate în fundal. Puteți să continuați utilizarea Miniflux în timp ce procesul rulează.",
"alert.feed_error": "Este o problemă cu acest flux",
"alert.no_bookmark": "Nu sunt înregistrări marcate.",
"alert.no_category": "Nu sunt categorii.",
"alert.no_category_entry": "Nu sunt înregistrări în această categorie.",
"alert.no_feed": "Nu aveți fluxuri.",
"alert.no_feed_entry": "Nu sunt înregistrări pentru acest flux.",
"alert.no_feed_in_category": "Nu sunt fluxuri pentru această categorie.",
"alert.no_history": "Nu există istoric în acest moment.",
"alert.no_search_result": "Nu există înregistrări pentru această căutare.",
"alert.no_shared_entry": "Nu sunt înregistrări partajate.",
"alert.no_tag_entry": "Nu sunt înregistrări pentru această etichetă.",
"alert.no_unread_entry": "Nu sunt intrări necitite.",
"alert.no_user": "Sunteți singurul utilizator.",
"alert.pocket_linked": "Contul dvs. Pocket este atașat!",
"alert.prefs_saved": "Preferințe salvate!",
"alert.too_many_feeds_refresh": [
"Ați activat actualizarea a prea multe fluxuri de informații. Vă rog să așteptați %d minut înainte de a reîncerca.",
"Ați activat actualizarea a prea multe fluxuri de informații. Vă rog să așteptați %d minute înainte de a reîncerca.",
"Ați activat actualizarea a prea multe fluxuri de informații. Vă rog să așteptați %d minute înainte de a reîncerca."
],
"confirm.loading": "În progres…",
"confirm.no": "nu",
"confirm.question": "Suneți sigur?",
"confirm.question.refresh": "Sunteți sigur că vreți să forțați reîmprospătarea?",
"confirm.yes": "da",
"enclosure_media_controls.seek": "Caută:",
"enclosure_media_controls.seek.title": "Caută %s secunde",
"enclosure_media_controls.speed": "Viteză:",
"enclosure_media_controls.speed.faster": "Mai rapid",
"enclosure_media_controls.speed.faster.title": "Mai rapid cu %sx",
"enclosure_media_controls.speed.reset": "Resetare",
"enclosure_media_controls.speed.reset.title": "Resetare viteză la 1x",
"enclosure_media_controls.speed.slower": "Mai încet",
"enclosure_media_controls.speed.slower.title": "Mai încet cu %sx",
"entry.bookmark.toast.off": "Fără stea",
"entry.bookmark.toast.on": "Cu stea",
"entry.bookmark.toggle.off": "Fără stea",
"entry.bookmark.toggle.on": "Stea",
"entry.comments.label": "Comentarii",
"entry.comments.title": "Vizualizare Comentarii",
"entry.estimated_reading_time": [
"%d minut de lectură",
"%d minute de lectură",
"%d minut de lectură"
],
"entry.external_link.label": "Legătură externă",
"entry.save.completed": "Gata!",
"entry.save.label": "Salvare",
"entry.save.title": "Salvez această înregistrare",
"entry.save.toast.completed": "Înregistrare salvată",
"entry.scraper.completed": "Gata!",
"entry.scraper.label": "Descărcare",
"entry.scraper.title": "Descarcă conținutul original",
"entry.share.label": "Partajare",
"entry.share.title": "Partajează această înregistrare",
"entry.shared_entry.label": "Partajare",
"entry.shared_entry.title": "Deschide legătura publică",
"entry.state.loading": "Încarc…",
"entry.state.saving": "Salvez…",
"entry.status.mark_as_read": "Marcați ca citit",
"entry.status.mark_as_unread": "Marcați ca necitit",
"entry.status.title": "Modifică starea intrării",
"entry.status.toast.read": "Marcat ca citit",
"entry.status.toast.unread": "Marcat ca necitit",
"entry.tags.label": "Etichete:",
"entry.unshare.label": "Elimină partajarea",
"error.api_key_already_exists": "Această cheie API există deja.",
"error.bad_credentials": "Utilizator sau parolă invalide.",
"error.category_already_exists": "Această categorie există deja.",
"error.category_not_found": "Această categorie nu există sau nu aparține acestui utilizator.",
"error.database_error": "Eroare bază de date: %v.",
"error.different_passwords": "Parolele nu sunt identice.",
"error.duplicate_fever_username": "Este deja cineva cu același cont de Fever!",
"error.duplicate_googlereader_username": "Este deja cineva cu același nume de utilizator Google Reader!",
"error.duplicate_linked_account": "Este deja cineva asociat cu acest furnizor!",
"error.duplicated_feed": "Acest flux există deja.",
"error.empty_file": "Acest fișier este gol.",
"error.entries_per_page_invalid": "Numărul de înregistrări de pe pagină nu este valid.",
"error.feed_already_exists": "Acest flux există deja.",
"error.feed_category_not_found": "Această categorie nu există sau nu aparține utilizatorului.",
"error.feed_format_not_detected": "Nu pot detecta formatul fluxului: %v.",
"error.feed_invalid_blocklist_rule": "Blocul listei de reguli este invalid.",
"error.feed_invalid_keeplist_rule": "Lista de reguli keep este invalidă.",
"error.feed_mandatory_fields": "Adresa URL și categoria sunt obligatorii.",
"error.feed_not_found": "Acest flux nu există sau un aparține acestui utilizator.",
"error.feed_title_not_empty": "Titlul fluxului nu poate fi gol.",
"error.feed_url_not_empty": "Adresa URL a fluxului nu poate fi goală.",
"error.fields_mandatory": "Toate câmpurile sunt obligatorii.",
"error.http_bad_gateway": "Acest site web nu este disponibil momentan din cauza unei erori generată de gateway. Problema nu este de la Miniflux. Vă rugăm să reîncercați mai târziu.",
"error.http_body_read": "Nu pot citi corpul HTTP: %v.",
"error.http_client_error": "Eroare client HTTP: %v.",
"error.http_empty_response": "Răspunsul HTTP este gol. Poate acest site web utilizează un mecanism împotriva boților?",
"error.http_empty_response_body": "Corpul răspunsului HTTP este gol.",
"error.http_forbidden": "Accesul la acest site web este interzis. Poate acesta utilizează un mecanism împotriva boților?",
"error.http_gateway_timeout": "Acest site web nu este disponibil momentan din cauza unei erori generată de gateway. Problema nu este de la Miniflux. Vă rugăm să reîncercați mai târziu.",
"error.http_internal_server_error": "Acest site web nu este disponibil momentan din cauza unei erori generată de server. Problema nu este de la Miniflux. Vă rugăm să reîncercați mai târziu.",
"error.http_not_authorized": "Accesul la acest site nu este autorizat. Poate fi din cauza parolei sau a userului greșite.",
"error.http_resource_not_found": "Resursa solicitată nu este găsită. Vă rog să verificați URL-ul.",
"error.http_response_too_large": "Răspunsul HTTP este prea mare. Puteți crește dimensiunea acestuia în setările globale (necesită repornirea server-ului).",
"error.http_service_unavailable": "Acest site web nu este disponibil momentan din cauza unei erori generată de server. Problema nu este de la Miniflux. Vă rugăm să reîncercați mai târziu.",
"error.http_too_many_requests": "Miniflux a generat prea multe solicitări pe acest site web. Vă rog, încercați mai tîrziu sau modificați configurațiile aplicației.",
"error.http_unexpected_status_code": "Acest site web nu este disponibil momentan din cauza unei erori HTTP: %d. Problema nu este de la Miniflux. Vă rugăm să reîncercați mai târziu.",
"error.invalid_default_home_page": "Pagină de start invalidă!",
"error.invalid_display_mode": "Mod invalid de afișare în aplicația web.",
"error.invalid_entry_direction": "Direcție invalidă ăn intrare.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Adresa URL a fluxului este invalidă.",
"error.invalid_gesture_nav": "Gest de navigare invalid.",
"error.invalid_language": "Limbă invalidă.",
"error.invalid_site_url": "Adresa URL a site-ului este invalidă.",
"error.invalid_theme": "Temă invalidă.",
"error.invalid_timezone": "Dată/oră invalide.",
"error.network_operation": "Miniflux nu poate ajunge la acest site din cauza unei erori de rețea: %v.",
"error.network_timeout": "Acest site web este prea lent și conexiunea nu s-a realizat: %v",
"error.password_min_length": "Parola trebuie să aibă cel puțin 6 caractere.",
"error.pocket_access_token": "Nu poate obține token-ul de acces de la Pocket!",
"error.pocket_request_token": "Nu poate obține token-ul solicitat de la Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Regulă de bloc invalidă: regulii #%d îi lipsește un nume valid de câmp (Opțiuni: %s)",
"error.settings_block_rule_invalid_regex": "Regulă de bloc invalidă: modelul regulii #%d's nu este regex valid",
"error.settings_block_rule_regex_required": "Regulă de bloc invalidă: modelul regulii #%d's nu este furnizat",
"error.settings_block_rule_separator_required": "Regulă de bloc invalidă: modelul regulii #%d's trebuie separat de '='",
"error.settings_invalid_domain_list": "Lista domeniilor invalidă. Vă rugăm să ne furnizați o listă separată de virgulă a domeniilor.",
"error.settings_keep_rule_fieldname_invalid": "Regulă Keep invalidă: regulii #%d îi lipsește un nume valid (Opțiuni: %s)",
"error.settings_keep_rule_invalid_regex": "Regulă Keep invalidă: modelul regulii #%d's nu este regex valid",
"error.settings_keep_rule_regex_required": "Regulă Keep invalidă: modelul regulii #%d nu este furnizat",
"error.settings_keep_rule_separator_required": "Regulă Keep invalidă: modelul regulii #%d's trebuie separat de'='",
"error.settings_mandatory_fields": "Numele utilizatorului, tema, limba și fusul orar sunt obligatorii.",
"error.settings_media_playback_rate_range": "Viteza de rulare nu este validă",
"error.settings_reading_speed_is_positive": "Vitezele de citire trebuie să fie numere întregi pozitive.",
"error.site_url_not_empty": "Adresa URL a site-ului nu poate fi goală.",
"error.subscription_not_found": "Nu se poate găsi nici un flux.",
"error.title_required": "Titlul este obligatoriu.",
"error.tls_error": "Eroare TLS: %q. Puteți dezactiva verificarea TLS în setările fluxurilor dacă doriți.",
"error.unable_to_create_api_key": "Nu pot crea această cheie API.",
"error.unable_to_create_category": "Nu se poate crea această categorie.",
"error.unable_to_create_user": "Nu se poate crea utilizatorul.",
"error.unable_to_detect_rssbridge": "Nu pot detecta fluxul când utilizez RSS-Bridge: %v.",
"error.unable_to_parse_feed": "Nu pot procesa acest flux: %v.",
"error.unable_to_update_category": "Nu se poate actualiza această categorie.",
"error.unable_to_update_feed": "Nu se poate actualiza acest flux.",
"error.unable_to_update_user": "Nu se poate actualiza utilizatorul.",
"error.unlink_account_without_password": "Trebuie să definiți o parolă, altfel nu vă veți mai putea conecta.",
"error.user_already_exists": "Acest utilizator există deja.",
"error.user_mandatory_fields": "Numele utilizatorului este obligatoriu.",
"form.api_key.label.description": "Etichetă Cheie API",
"form.category.hide_globally": "Ascunde intrările în lista globală de articole necitite",
"form.category.label.title": "Titlu",
"form.feed.fieldset.general": "General",
"form.feed.fieldset.integration": "Servicii Terțe",
"form.feed.fieldset.network_settings": "Setări Rețea",
"form.feed.fieldset.rules": "Reguli",
"form.feed.label.allow_self_signed_certificates": "Permite certificatele auto-semnate sau invalide",
"form.feed.label.apprise_service_urls": "Lista de URL-uri ale serviciilor Apprise separate prin virgule",
"form.feed.label.blocklist_rules": "Reguli de Blocare",
"form.feed.label.category": "Categorie",
"form.feed.label.cookie": "Setare Cookie-uri",
"form.feed.label.crawler": "Aduce conținutul original",
"form.feed.label.description": "Descriere",
"form.feed.label.disable_http2": "Dezactivează HTTP/2 pentru a preveni amprentarea",
"form.feed.label.disabled": "Nu actualiza acest flux",
"form.feed.label.feed_password": "Parolă Flux",
"form.feed.label.feed_url": "Flux URL",
"form.feed.label.feed_username": "Nume user Flux",
"form.feed.label.fetch_via_proxy": "Utilizați proxy-ul configurat la nivelul aplicației",
"form.feed.label.hide_globally": "Ascunde intrările în lista globală de articole necitite",
"form.feed.label.ignore_http_cache": "Ignoră cache HTTP",
"form.feed.label.keeplist_rules": "Reguli de Păstrare",
"form.feed.label.no_media_player": "Nu există player media (audio/video)",
"form.feed.label.ntfy_activate": "Împinge intrările la ntfy",
"form.feed.label.ntfy_default_priority": "Prioritate predefinită Ntfy",
"form.feed.label.ntfy_high_priority": "Prioritate ridicată Ntfy",
"form.feed.label.ntfy_low_priority": "Prioritate redusă Ntfy",
"form.feed.label.ntfy_max_priority": "Prioritate maximă Ntfy",
"form.feed.label.ntfy_min_priority": "Prioritate minimă Ntfy",
"form.feed.label.ntfy_priority": "Prioritate Ntfy",
"form.feed.label.ntfy_topic": "Subiect Ntfy (opțional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Activează Pushover",
"form.feed.label.pushover_default_priority": "Prioritate implicită Pushover",
"form.feed.label.pushover_high_priority": "Prioritate ridicată Pushover",
"form.feed.label.pushover_low_priority": "Prioritate redusă Pushover",
"form.feed.label.pushover_max_priority": "Prioritate maximă Pushover",
"form.feed.label.pushover_min_priority": "Prioritate minimă Pushover",
"form.feed.label.pushover_priority": "Prioritate Pushover",
"form.feed.label.rewrite_rules": "Rescrie Regulile",
"form.feed.label.scraper_rules": "Reguli de Eliminare",
"form.feed.label.site_url": "Adresă URL",
"form.feed.label.title": "Titlu",
"form.feed.label.urlrewrite_rules": "URL Reguli de Rescriere",
"form.feed.label.user_agent": "Suprascrie User Agent Predefinit",
"form.feed.label.webhook_url": "URL Webhook (pentru a primi notificări despre evenimentele de intrare)",
"form.import.label.file": "Fișier OPML",
"form.import.label.url": "URL",
"form.integration.apprise_activate": "Trimite înregistrările pe Apprise",
"form.integration.apprise_services_url": "URL-uri separate de virgulă cu servicii Apprise",
"form.integration.apprise_url": "URL API Apprise",
"form.integration.betula_activate": "Salvează înregistrările în Betula",
"form.integration.betula_token": "Token Betula",
"form.integration.betula_url": "Adresă server Betula",
"form.integration.cubox_activate": "Salvează intrările în Cubox",
"form.integration.cubox_api_link": "Link APi Cubox",
"form.integration.discord_activate": "Împinge intrările pe Discord",
"form.integration.discord_webhook_link": "Link Webhook Discord",
"form.integration.espial_activate": "Save entries to Espial",
"form.integration.espial_api_key": "Cheie API Espial",
"form.integration.espial_endpoint": "Punct acces API Espial",
"form.integration.espial_tags": "Etichete Espial",
"form.integration.fever_activate": "Activează API Fever",
"form.integration.fever_endpoint": "Punct access API Fever:",
"form.integration.fever_password": "Parolă Fever",
"form.integration.fever_username": "Utilizator Fever",
"form.integration.googlereader_activate": "Activează API Google Reader",
"form.integration.googlereader_endpoint": "Punct acces API Google Reader:",
"form.integration.googlereader_password": "Parolă Google Reader",
"form.integration.googlereader_username": "Utilizator Google Reader",
"form.integration.instapaper_activate": "Salvează înregistrările pe Instapaper",
"form.integration.instapaper_password": "Parolă Instapaper",
"form.integration.instapaper_username": "Utilizator Instapaper",
"form.integration.linkace_activate": "Salvează intrările în LinkAce",
"form.integration.linkace_api_key": "Cheie API LinkAce",
"form.integration.linkace_check_disabled": "Dezactivează verificarea link-urilor",
"form.integration.linkace_endpoint": "Endpoint API LinkAce",
"form.integration.linkace_is_private": "Marchează link-urile ca private",
"form.integration.linkace_tags": "Tag-uri LinkAce",
"form.integration.linkding_activate": "Salvează intrările în Linkding",
"form.integration.linkding_api_key": "Cheie API Linkding",
"form.integration.linkding_bookmark": "Marchează semnele de carte ca necitite",
"form.integration.linkding_endpoint": "Endpoint API Linkding",
"form.integration.linkding_tags": "TAG-uri Linkding",
"form.integration.linkwarden_activate": "Salvează intrările în Linkwarden",
"form.integration.linkwarden_api_key": "Cheie API Linkwarden",
"form.integration.linkwarden_endpoint": "Endpoint API Linkwarden",
"form.integration.matrix_bot_activate": "Împinge intrările noi pe Matrix",
"form.integration.matrix_bot_chat_id": "ID-ul Camerei Matrix",
"form.integration.matrix_bot_password": "Parola utilizatorului Matrix",
"form.integration.matrix_bot_url": "Server URL Matrix",
"form.integration.matrix_bot_user": "Utilizator Matrix",
"form.integration.notion_activate": "Salvează înregistrările în Notion",
"form.integration.notion_page_id": "ID Pagină Notion",
"form.integration.notion_token": "Token Secret Notion",
"form.integration.ntfy_activate": "Împinge intrările pe ntfy",
"form.integration.ntfy_api_token": "Token API Ntfy (opțional)",
"form.integration.ntfy_icon_url": "Icon URL Ntfy (opțional)",
"form.integration.ntfy_internal_links": "Utilizează legături interne la clic (opțional)",
"form.integration.ntfy_password": "Parolă Ntfy (opțional)",
"form.integration.ntfy_topic": "Topic Ntfy",
"form.integration.ntfy_url": "URL Ntfy (opțional, predefinit este ntfy.sh)",
"form.integration.ntfy_username": "Utilizator Ntfy (opțional)",
"form.integration.nunux_keeper_activate": "Salvează înregistrările în Nunux Keeper",
"form.integration.nunux_keeper_api_key": "Cheie API Nunux Keeper",
"form.integration.nunux_keeper_endpoint": "Punct de acces API Keeper",
"form.integration.omnivore_activate": "Salvare înregistrări în Omnivore",
"form.integration.omnivore_api_key": "Cheie API Omnivore",
"form.integration.omnivore_url": "Punct acces API Omnivore",
"form.integration.pinboard_activate": "Salvează intrările în Pinboard",
"form.integration.pinboard_bookmark": "Marchează bookmark ca necitit",
"form.integration.pinboard_tags": "Etichete Pinboard",
"form.integration.pinboard_token": "Token API Pinboard",
"form.integration.pocket_access_token": "Token Acces Pocket",
"form.integration.pocket_activate": "Salvează înregistrările în Pocket",
"form.integration.pocket_connect_link": "Conectează contul Pocket personal",
"form.integration.pocket_consumer_key": "Cheie Consumator Pocket",
"form.integration.pushover_activate": "Activează Pushover",
"form.integration.pushover_device": "Dispozitiv Pushover (opțional)",
"form.integration.pushover_prefix": "Prefix Pushover (opțional)",
"form.integration.pushover_token": "Token Pushover",
"form.integration.pushover_user": "Utilizator Pushover",
"form.integration.raindrop_activate": "Salvează intrările în Raindrop",
"form.integration.raindrop_collection_id": "ID Colecție",
"form.integration.raindrop_tags": "Tag-uri (separate de virgulă)",
"form.integration.raindrop_token": "(Test) Token",
"form.integration.readeck_activate": "Salvează intrările în readeck",
"form.integration.readeck_api_key": "Cheie API Readeck",
"form.integration.readeck_endpoint": "URL Readeck",
"form.integration.readeck_labels": "Etichete Readeck",
"form.integration.readeck_only_url": "Trimite numai URL (în loc de tot conținutul)",
"form.integration.readwise_activate": "Salvare înregistrări în Readwise Reader",
"form.integration.readwise_api_key": "Token Acces Readwise Reader",
"form.integration.readwise_api_key_link": "Obțineți Token-ul de Acess pe Readwise",
"form.integration.rssbridge_activate": "Verifică RSS-Bridge la adăugarea de abonamente",
"form.integration.rssbridge_url": "URL server RSS-Bridge",
"form.integration.shaarli_activate": "Salvează articolele în Shaarli",
"form.integration.shaarli_api_secret": "Secret API Shaarli",
"form.integration.shaarli_endpoint": "URL Shaarli",
"form.integration.shiori_activate": "Salvează articolele în Shiori",
"form.integration.shiori_endpoint": "Endpoint API Shiori",
"form.integration.shiori_password": "Parolă Shiori",
"form.integration.shiori_username": "Utilizator Shiori",
"form.integration.slack_activate": "Împinge intrările pe Slack",
"form.integration.slack_webhook_link": "Link Webhook Slack",
"form.integration.telegram_bot_activate": "Împingeți înregistrările noi pe chat-ul Telegram",
"form.integration.telegram_bot_disable_buttons": "Dezactivează butoanele",
"form.integration.telegram_bot_disable_notification": "Dezactivează notificările",
"form.integration.telegram_bot_disable_web_page_preview": "Dezactivează previzualizarea paginii web",
"form.integration.telegram_bot_token": "Token Bot",
"form.integration.telegram_chat_id": "ID Chat",
"form.integration.telegram_topic_id": "ID Topic",
"form.integration.wallabag_activate": "Salvează înregistrările în Wallabag",
"form.integration.wallabag_client_id": "ID Client Wallabag",
"form.integration.wallabag_client_secret": "Secret Client Wallabag",
"form.integration.wallabag_endpoint": "URL Wallabag",
"form.integration.wallabag_only_url": "Trimite numai URL-ul (fără conținut complet)",
"form.integration.wallabag_password": "Parolă Wallabag",
"form.integration.wallabag_username": "Utilizator Wallabag",
"form.integration.webhook_activate": "Activează Webhook",
"form.integration.webhook_secret": "Secret Webhook",
"form.integration.webhook_url": "URL Webhook",
"form.prefs.fieldset.application_settings": "Setări Aplicație",
"form.prefs.fieldset.authentication_settings": "Setări Autentificare",
"form.prefs.fieldset.global_feed_settings": "Setări Globale pt. Flux",
"form.prefs.fieldset.reader_settings": "Setări Citire",
"form.prefs.help.external_font_hosts": "Lista fonturilor de pe gazdă separate de virgulă care poate fi utilizate. De exemplu: \"fonts.gstatic.com fonts.googleapis.com\".",
"form.prefs.label.categories_sorting_order": "Sortare categorii",
"form.prefs.label.cjk_reading_speed": "Viteză de citire pentru Chineză, Coreană și Japoneză (caractere pe minut)",
"form.prefs.label.custom_css": "CSS personalizat",
"form.prefs.label.custom_js": "JavaScript personalizat",
"form.prefs.label.default_home_page": "Pagina pornire predefinită",
"form.prefs.label.default_reading_speed": "Viteză de citire pentru alte limbi (cuvinte pe minut)",
"form.prefs.label.display_mode": "Mod afișare Aplicație Web Progresivă (PWA)",
"form.prefs.label.entries_per_page": "Intrări pe pagină",
"form.prefs.label.entry_order": "Coloană de sortare",
"form.prefs.label.entry_sorting": "Sortare intrări",
"form.prefs.label.entry_swipe": "Activare glisare pentru ecranele tactile",
"form.prefs.label.external_font_hosts": "Fonturi externe gazdă",
"form.prefs.label.gesture_nav": "Gesturi pentru navigare între înregistrări",
"form.prefs.label.keyboard_shortcuts": "Activare scurtături tastatură",
"form.prefs.label.language": "Limbă",
"form.prefs.label.mark_read_manually": "Marchează manual intrările ca citite",
"form.prefs.label.mark_read_on_media_completion": "Marchează ca citit numai când redarea de conținut audio/video atinge 90%%",
"form.prefs.label.mark_read_on_view": "Marchează intrările ca citite la vizualizare",
"form.prefs.label.mark_read_on_view_or_media_completion": "Marchează intrările ca citite la vizualizare. Pentru audio/video, marchează ca citit la redarea a 90%% de conținut",
"form.prefs.label.media_playback_rate": "Viteza de rulare audio/video",
"form.prefs.label.show_reading_time": "Afișare timp estimat de citire pentru înregistrări",
"form.prefs.label.theme": "Temă",
"form.prefs.label.timezone": "Fus orar",
"form.prefs.select.alphabetical": "Alfabetic",
"form.prefs.select.browser": "Browser",
"form.prefs.select.created_time": "Dată creare înregistrare",
"form.prefs.select.fullscreen": "Ecran complet",
"form.prefs.select.minimal_ui": "Minim",
"form.prefs.select.none": "Nimic",
"form.prefs.select.older_first": "Intrările mai vechi la început",
"form.prefs.select.publish_time": "Data publicare înregistrare",
"form.prefs.select.recent_first": "Intrările mai noi la început",
"form.prefs.select.standalone": "Independent",
"form.prefs.select.swipe": "Glisare",
"form.prefs.select.tap": "Apăsare dublă",
"form.prefs.select.unread_count": "Contor necitite",
"form.submit.loading": "Încarc…",
"form.submit.saving": "Salvez…",
"form.user.label.admin": "Administrator",
"form.user.label.confirmation": "Confirmare Parolă",
"form.user.label.password": "Parolă",
"form.user.label.username": "Nume utilizator",
"menu.about": "Despre",
"menu.add_feed": "Adaugă flux",
"menu.add_user": "Adaugă utilizator",
"menu.api_keys": "Chei API",
"menu.categories": "Categorii",
"menu.create_api_key": "Crează o nouă cheie API",
"menu.create_category": "Crează o categorie",
"menu.edit_category": "Editare",
"menu.edit_feed": "Editare",
"menu.export": "Exportă",
"menu.feed_entries": "Intrări",
"menu.feeds": "Fluxuri",
"menu.flush_history": "Elimină istoricul",
"menu.history": "Istoric",
"menu.home_page": "Pagina principală",
"menu.import": "Importă",
"menu.integrations": "Integrări",
"menu.logout": "Deconectare",
"menu.mark_all_as_read": "Marchează tot ca citit",
"menu.mark_page_as_read": "Marchează această pagină ca citită",
"menu.preferences": "Preferințe",
"menu.refresh_all_feeds": "Reînnoiește toate fluxurile în fundal",
"menu.refresh_feed": "Reînnoire",
"menu.search": "Caută",
"menu.sessions": "Sesiuni",
"menu.settings": "Setări",
"menu.shared_entries": "Intrări partajate",
"menu.show_all_entries": "Afișează toate intrările",
"menu.show_only_starred_entries": "Afișează numai intrările marcate",
"menu.show_only_unread_entries": "Afișează numai intrările necitite",
"menu.starred": "Marcat",
"menu.title": "Meniu",
"menu.unread": "Necitit",
"menu.users": "Utilizatori",
"page.about.author": "Autor:",
"page.about.build_date": "Dată Build:",
"page.about.credits": "Credit",
"page.about.db_usage": "Utilizare Bază de Date",
"page.about.global_config_options": "Opțiuni globale de configurare",
"page.about.go_version": "Versiune Go:",
"page.about.license": "Licență:",
"page.about.postgres_version": "Versiune Postgres:",
"page.about.title": "Despre",
"page.about.version": "Versiune:",
"page.add_feed.choose_feed": "Alegeți un flux",
"page.add_feed.label.url": "URL",
"page.add_feed.legend.advanced_options": "Opțiuni Avansate",
"page.add_feed.no_category": "Nu există categorii. Trebuie să aveți măcar o categorie.",
"page.add_feed.submit": "Găsește un flux",
"page.add_feed.title": "Flux nou",
"page.api_keys.never_used": "Niciodată Utilizată",
"page.api_keys.table.actions": "Acțiuni",
"page.api_keys.table.created_at": "Dată Creare",
"page.api_keys.table.description": "Descriere",
"page.api_keys.table.last_used_at": "Utilizat ultima dată",
"page.api_keys.table.token": "Token",
"page.api_keys.title": "Chei API",
"page.categories_count": [
"%d categorie",
"%d categorii",
"%d categorie găsită"
],
"page.categories.entries": "Intrări",
"page.categories.feed_count": [
"Este %d flux.",
"Sunt %d fluxuri.",
"Sunt %d fluxuri găsite."
],
"page.categories.feeds": "Fluxuri",
"page.categories.no_feed": "Nici un flux.",
"page.categories.title": "Categorii",
"page.category_label": "Categorie: %s",
"page.edit_category.title": "Editare Categorie: %s",
"page.edit_feed.etag_header": "Antet ETag:",
"page.edit_feed.last_check": "Ultima verificare:",
"page.edit_feed.last_modified_header": "UltimaModificare antet:",
"page.edit_feed.last_parsing_error": "Ultima Eroare la Analiză",
"page.edit_feed.no_header": "Nimic",
"page.edit_feed.title": "Editare Flux: %s",
"page.edit_user.title": "Editare Utilizator: %s",
"page.entry.attachments": "Atașamente",
"page.feeds.error_count": [
"%d eroare",
"%d erori",
"%d erori găsite"
],
"page.feeds.last_check": "Ultima verificare:",
"page.feeds.next_check": "Următoarea verificare:",
"page.feeds.read_counter": "Numărul de intrări citite",
"page.feeds.title": "Fluxuri",
"page.history.title": "Istoric",
"page.import.title": "Import",
"page.integration.bookmarklet": "Marcaje",
"page.integration.bookmarklet.help": "Această legătură specială permite să vă abonați direct pe un site prin utilizarea unui marcaj în browser-ul web.",
"page.integration.bookmarklet.instructions": "Trageți legătura în favorite.",
"page.integration.bookmarklet.name": "Adaugă în Miniflux",
"page.integration.miniflux_api": "API Miniflux",
"page.integration.miniflux_api_endpoint": "Punct de acces API",
"page.integration.miniflux_api_password": "Parolă",
"page.integration.miniflux_api_password_value": "Parola contului",
"page.integration.miniflux_api_username": "Utilizator",
"page.integrations.title": "Integrări",
"page.keyboard_shortcuts.close_modal": "Închide fereastra de dialog",
"page.keyboard_shortcuts.download_content": "Descarcă conținutul original",
"page.keyboard_shortcuts.go_to_bottom_item": "Du-te la ultimul obiect",
"page.keyboard_shortcuts.go_to_categories": "Du-te la categorii",
"page.keyboard_shortcuts.go_to_feed": "Du-te la flux",
"page.keyboard_shortcuts.go_to_feeds": "Du-te la fluxuri",
"page.keyboard_shortcuts.go_to_history": "Du-te la istoric",
"page.keyboard_shortcuts.go_to_next_item": "Du-te la obiectul următor",
"page.keyboard_shortcuts.go_to_next_page": "Du-te la pagina următoare",
"page.keyboard_shortcuts.go_to_previous_item": "Du-te la obiectul anterior",
"page.keyboard_shortcuts.go_to_previous_page": "Du-te la pagina anterioară",
"page.keyboard_shortcuts.go_to_search": "Focusul pe formularul de căutare",
"page.keyboard_shortcuts.go_to_settings": "Du-te la setări",
"page.keyboard_shortcuts.go_to_starred": "Du-te la marcat",
"page.keyboard_shortcuts.go_to_top_item": "Du-te la primul obiect",
"page.keyboard_shortcuts.go_to_unread": "Du-te la necitit",
"page.keyboard_shortcuts.mark_page_as_read": "Marchează pagina curentă ca citită",
"page.keyboard_shortcuts.open_comments": "Deschide comentariile link-ului",
"page.keyboard_shortcuts.open_comments_same_window": "Deschide comentariile link-ului în tab-ul curent",
"page.keyboard_shortcuts.open_item": "Deschide obiectul selectat",
"page.keyboard_shortcuts.open_original": "Deschide link-ul original",
"page.keyboard_shortcuts.open_original_same_window": "Deschide link-ul original în tab-ul curent",
"page.keyboard_shortcuts.refresh_all_feeds": "Reîncarcă toate fluxurile în fundal",
"page.keyboard_shortcuts.remove_feed": "Elimină acest flux",
"page.keyboard_shortcuts.save_article": "Salvare înregistrare",
"page.keyboard_shortcuts.scroll_item_to_top": "Derulează obiectul la început",
"page.keyboard_shortcuts.show_keyboard_shortcuts": "Afișează scurtăturile tastaturii",
"page.keyboard_shortcuts.subtitle.actions": "Acțiuni",
"page.keyboard_shortcuts.subtitle.items": "Navigare Obiecte",
"page.keyboard_shortcuts.subtitle.pages": "Navigare Pagini",
"page.keyboard_shortcuts.subtitle.sections": "Navigare Secțiuni",
"page.keyboard_shortcuts.title": "Scurtături Tastatură",
"page.keyboard_shortcuts.toggle_bookmark_status": "Comută marcate",
"page.keyboard_shortcuts.toggle_entry_attachments": "Comută deschis/închis pe atașamentele înregistrării",
"page.keyboard_shortcuts.toggle_read_status_next": "Comută citit/necitit focus următor",
"page.keyboard_shortcuts.toggle_read_status_prev": "Comută citit/necitit, focus anterior",
"page.login.google_signin": "Conectare cu Google",
"page.login.oidc_signin": "Conectare cu %s",
"page.login.title": "Conectare",
"page.login.webauthn_login": "Conectare cu cheia de acces",
"page.login.webauthn_login.error": "Eroare la conectarea cu cheia de acces",
"page.login.webauthn_login.help": "Vă rog să introduceți numele utilizatorului dacă utilizați o cheie. Nu este necesară dacă utilizați o cheie de acces (credențiale descoperibile).",
"page.new_api_key.title": "Cheie API Nouă",
"page.new_category.title": "Categorie Nouă",
"page.new_user.title": "Utilizator Nou",
"page.offline.message": "Sunteți offline",
"page.offline.refresh_page": "Încercați să reîmprospătați pagina",
"page.offline.title": "Mod Offline",
"page.read_entry_count": [
"%d înregistrare citită",
"%d înregistrări citite",
"%d înregistrări citite"
],
"page.search.title": "Rezultate Căutare",
"page.sessions.table.actions": "Acțiuni",
"page.sessions.table.current_session": "Sesiunea Curentă",
"page.sessions.table.date": "Dată",
"page.sessions.table.ip": "Adresă IP",
"page.sessions.table.user_agent": "Agent Utilizator",
"page.sessions.title": "Sesiuni",
"page.settings.link_google_account": "Atașează contul personal Google",
"page.settings.link_oidc_account": "Atașează contul meu %s",
"page.settings.title": "Setări",
"page.settings.unlink_google_account": "Decuplează contul personal Google",
"page.settings.unlink_oidc_account": "Decuplează contul meu %s",
"page.settings.webauthn.actions": "Acțiuni",
"page.settings.webauthn.added_on": "Adăugată în",
"page.settings.webauthn.delete": [
"Elimină %d cheie de acces",
"Elimină %d chei de acces",
"Elimină %d chei de acces"
],
"page.settings.webauthn.last_seen_on": "Utilizat ultima dată",
"page.settings.webauthn.passkey_name": "Nume cheie acces",
"page.settings.webauthn.passkeys": "Chei Acces",
"page.settings.webauthn.register": "Înregistrare cheie acces",
"page.settings.webauthn.register.error": "Eroare la înregistrarea cheii de acces",
"page.shared_entries_count": [
"%d înregistrare partajată",
"%d înregistrări partajate",
"%d înregistrări partajate"
],
"page.shared_entries.title": "Înregistrări partajate",
"page.starred_entry_count": [
"%d înregistrare marcată",
"%d Înregistrări marcate",
"%d Înregistrări marcate"
],
"page.starred.title": "Marcate",
"page.total_entry_count": [
"%d intrare în total",
"%d intrări în total",
"%d intrări în total"
],
"page.unread_entry_count": [
"%d înregistrare necitită",
"%d înregistrări necitite",
"%d înregistrări necitite"
],
"page.unread.title": "Necitite",
"page.users.actions": "Acțiuni",
"page.users.admin.no": "Nu",
"page.users.admin.yes": "Da",
"page.users.is_admin": "Administrator",
"page.users.last_login": "Ultima Conectare",
"page.users.never_logged": "Niciodată",
"page.users.title": "Utilizatori",
"page.users.username": "Nume",
"page.webauthn_rename.title": "Redenumire Cheie Acces",
"pagination.first": "Prima",
"pagination.last": "Ultima",
"pagination.next": "Următor",
"pagination.previous": "Anterior",
"search.label": "Caută",
"search.placeholder": "Caută…",
"search.submit": "Caută",
"skip_to_content": "Sari la conținut",
"time_elapsed.days": [
"%d zi în urmă",
"%d zile în urmă",
"%d zile în urmă"
],
"time_elapsed.hours": [
"%d oră în urmă",
"%d ore în urmă",
"%d ore în urmă"
],
"time_elapsed.minutes": [
"%d minut în urmă",
"%d minute în urmă",
"%d minute în urmă"
],
"time_elapsed.months": [
"%d lună în urmă",
"%d luni în urmă",
"%d luni în urmă"
],
"time_elapsed.not_yet": "încă nu",
"time_elapsed.now": "chiar acum",
"time_elapsed.weeks": [
"%d săptămână în urmă",
"%d săptămâni în urmă",
"%d săptămâni în urmă"
],
"time_elapsed.years": [
"%d an în urmă",
"%d ani în urmă",
"%d ani în urmă"
],
"time_elapsed.yesterday": "ieri",
"tooltip.keyboard_shortcuts": "Scurtături Tastatură: %s",
"tooltip.logged_user": "Atentificat ca %s"
}
+9 -4
View File
@@ -73,11 +73,11 @@
"entry.shared_entry.title": "Открыть публичную ссылку",
"entry.state.loading": "Загрузка…",
"entry.state.saving": "Сохранение…",
"entry.status.read": "Прочитано",
"entry.status.mark_as_read": "Отметить как прочитанное",
"entry.status.mark_as_unread": "Пометить как непрочитанное",
"entry.status.title": "Изменить статус записи",
"entry.status.toast.read": "Помечено как прочитанное",
"entry.status.toast.unread": "Помечено как непрочитанное",
"entry.status.unread": "Не прочитано",
"entry.tags.label": "Теги:",
"entry.unshare.label": "Удалить из общедоступных",
"error.api_key_already_exists": "Этот API-ключ уже существует.",
@@ -119,6 +119,7 @@
"error.invalid_default_home_page": "Недопустимая домашняя страница по умолчанию!",
"error.invalid_display_mode": "Недопустимый режим отображения веб-приложения.",
"error.invalid_entry_direction": "Недопустимая сортировка записей.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Недействительная ссылка подписки.",
"error.invalid_gesture_nav": "Недопустимая навигация жестами.",
"error.invalid_language": "Недопустимый язык.",
@@ -130,6 +131,7 @@
"error.password_min_length": "Вы должны использовать минимум 6 символов.",
"error.pocket_access_token": "Не удалось получить ключ доступа от Pocket!",
"error.pocket_request_token": "Не удалось получить request token от Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -176,7 +178,7 @@
"form.feed.label.feed_password": "Пароль подписки",
"form.feed.label.feed_url": "Адрес подписки",
"form.feed.label.feed_username": "Имя пользователя подписки",
"form.feed.label.fetch_via_proxy": "Использовать прокси",
"form.feed.label.fetch_via_proxy": "Использовать прокси, настроенный на уровне приложения",
"form.feed.label.hide_globally": "Скрыть записи в глобальном списке непрочитанных",
"form.feed.label.ignore_http_cache": "Игнорировать HTTP кеш",
"form.feed.label.keeplist_rules": "Правила белого списка",
@@ -188,6 +190,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -256,7 +260,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (default if not set in feed)",
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.nunux_keeper_activate": "Сохранять статьи в Nunux Keeper",
@@ -402,6 +406,7 @@
"page.about.author": "Автор:",
"page.about.build_date": "Дата сборки:",
"page.about.credits": "Авторы",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "Глобальные параметры конфигурации",
"page.about.go_version": "Версия Go:",
"page.about.license": "Лицензия:",
+11 -6
View File
@@ -71,11 +71,11 @@
"entry.shared_entry.title": "Herkese açık bağlantıyı aç",
"entry.state.loading": "Yükleniyor...",
"entry.state.saving": "Kaydediliyor...",
"entry.status.read": "Okundu",
"entry.status.mark_as_read": "Okundu olarak işaretle",
"entry.status.mark_as_unread": "Okunmadı olarak işaretle",
"entry.status.title": "Makele okundu durumunu değiştir",
"entry.status.toast.read": "Okundu olarak işaretle",
"entry.status.toast.unread": "Okunmadı olarak işaretle",
"entry.status.unread": "Okunmadı",
"entry.status.toast.read": "Okundu olarak işaretlendi",
"entry.status.toast.unread": "Okunmamış olarak işaretlendi",
"entry.tags.label": "Etiketler:",
"entry.unshare.label": "Paylaşma",
"error.api_key_already_exists": "Bu API anahtarı zaten mevcut.",
@@ -117,6 +117,7 @@
"error.invalid_default_home_page": "Geçersiz varsayılan ana sayfa!",
"error.invalid_display_mode": "Geçersiz web uygulaması görüntüleme modu.",
"error.invalid_entry_direction": "Geçersiz makele sıralaması.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Geçersiz besleme URL'si.",
"error.invalid_gesture_nav": "Hareketle gezinme geçersiz.",
"error.invalid_language": "Geçersiz dil.",
@@ -128,6 +129,7 @@
"error.password_min_length": "Parola en az 6 karakter içermeli.",
"error.pocket_access_token": "Pocket'tan access tokeni alınamıyor!",
"error.pocket_request_token": "Pocket'tan request tokeni alınamıyor!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Geçersiz Engelleme kuralı: #%d kuralında geçerli bir alan adı eksik (Seçenekler: %s)",
"error.settings_block_rule_invalid_regex": "Geçersiz Engelleme kuralı: #%d kuralı modeli geçerli bir düzenli ifade değil",
"error.settings_block_rule_regex_required": "Geçersiz Engelleme kuralı: #%d kuralı modeli sağlanmadı",
@@ -174,7 +176,7 @@
"form.feed.label.feed_password": "Besleme Parolası",
"form.feed.label.feed_url": "Besleme URL'si",
"form.feed.label.feed_username": "Besleme Kullanıcı Adı",
"form.feed.label.fetch_via_proxy": "Proxy ile çek",
"form.feed.label.fetch_via_proxy": "Uygulama düzeyinde yapılandırılmış proxy'yi kullan",
"form.feed.label.hide_globally": "Genel okunmamış listesindeki girişleri gizle",
"form.feed.label.ignore_http_cache": "HTTP önbelleğini yoksay",
"form.feed.label.keeplist_rules": "Saklama Kuralları",
@@ -186,6 +188,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -254,7 +258,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (default if not set in feed)",
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.nunux_keeper_activate": "Makaleleri Nunux Keeper'a kaydet",
@@ -400,6 +404,7 @@
"page.about.author": "Yazar:",
"page.about.build_date": "Oluşturulma Tarihi:",
"page.about.credits": "Katkıda Bulunanlar",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "Global yapılandırma seçenekleri",
"page.about.go_version": "Go sürümü:",
"page.about.license": "Lisans:",
+9 -4
View File
@@ -73,11 +73,11 @@
"entry.shared_entry.title": "Відкрити публічне посилання",
"entry.state.loading": "Завантаження...",
"entry.state.saving": "Зберігаю...",
"entry.status.read": "Прочитане",
"entry.status.mark_as_read": означити як прочитане",
"entry.status.mark_as_unread": "Позначити як непрочитане",
"entry.status.title": "Змінити стан запису",
"entry.status.toast.read": "Відмічено прочитаним",
"entry.status.toast.unread": "Відмічено непрочитаним",
"entry.status.unread": "Непрочитане",
"entry.tags.label": "Теги:",
"entry.unshare.label": "Не ділитися",
"error.api_key_already_exists": "Такий ключ API вже існує.",
@@ -119,6 +119,7 @@
"error.invalid_default_home_page": "Недійсна домашня сторінка за замовчуванням!",
"error.invalid_display_mode": "Недійсний режим відображення.",
"error.invalid_entry_direction": "Недійсний напрямок запису.",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "Недійсна URL-адреса стрічки.",
"error.invalid_gesture_nav": "Недійсна навігація жестами.",
"error.invalid_language": "Недійсна мова.",
@@ -130,6 +131,7 @@
"error.password_min_length": "Пароль має складати щонайменше 6 символів.",
"error.pocket_access_token": "Не вдалося отримати токен доступу з Pocket!",
"error.pocket_request_token": "Не вдалося отримати токен доступу з Pocket!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "Invalid Block rule: rule #%d is missing a valid field name (Options: %s)",
"error.settings_block_rule_invalid_regex": "Invalid Block rule: rule #%d's pattern is not a valid regex",
"error.settings_block_rule_regex_required": "Invalid Block rule: rule #%d's pattern is not provided",
@@ -176,7 +178,7 @@
"form.feed.label.feed_password": "Пароль для завантаження",
"form.feed.label.feed_url": "URL-адреса стрічки",
"form.feed.label.feed_username": "Ім’я користувача для завантаження",
"form.feed.label.fetch_via_proxy": "Використати проксі-сервер",
"form.feed.label.fetch_via_proxy": "Використовувати проксі, налаштований на рівні програми",
"form.feed.label.hide_globally": "Приховати записи в глобальному списку непрочитаного",
"form.feed.label.ignore_http_cache": "Ігнорувати кеш HTTP",
"form.feed.label.keeplist_rules": "Правила дозволення",
@@ -188,6 +190,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy max priority",
"form.feed.label.ntfy_min_priority": "Ntfy min priority",
"form.feed.label.ntfy_priority": "Ntfy priority",
"form.feed.label.ntfy_topic": "Ntfy topic (optional)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -256,7 +260,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon URL (optional)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy Password (optional)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (default if not set in feed)",
"form.integration.ntfy_url": "Ntfy URL (optional, default is ntfy.sh)",
"form.integration.ntfy_username": "Ntfy Username (optional)",
"form.integration.nunux_keeper_activate": "Зберігати статті до Nunux Keeper",
@@ -402,6 +406,7 @@
"page.about.author": "Автор:",
"page.about.build_date": "Дата побудови:",
"page.about.credits": "Титри",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "Параметри глобальної конфігурації",
"page.about.go_version": "Версія Go:",
"page.about.license": "Ліцензія:",
+59 -54
View File
@@ -13,7 +13,7 @@
"action.update": "更新",
"alert.account_linked": "您的外部账号已关联!",
"alert.account_unlinked": "您的外部帐户现已解除关联!",
"alert.background_feed_refresh": "所有订阅源在后台刷新中。您可以继续使用Miniflux,同时此过程正在运行。",
"alert.background_feed_refresh": "所有订阅源在后台更新。此过程中您仍可继续使用 Miniflux。",
"alert.feed_error": "该源存在问题",
"alert.no_bookmark": "目前没有收藏",
"alert.no_category": "目前没有分类",
@@ -69,11 +69,11 @@
"entry.shared_entry.title": "打开公共链接",
"entry.state.loading": "载入中…",
"entry.state.saving": "保存中…",
"entry.status.read": "标为已读",
"entry.status.mark_as_read": "标为已读",
"entry.status.mark_as_unread": "标为未读",
"entry.status.title": "更改状态",
"entry.status.toast.read": "已标为已读",
"entry.status.toast.unread": "已标为未读",
"entry.status.unread": "标为未读",
"entry.tags.label": "标签:",
"entry.unshare.label": "取消分享",
"error.api_key_already_exists": "此 API 密钥已存在。",
@@ -98,34 +98,36 @@
"error.feed_title_not_empty": "订阅源的标题不能为空。",
"error.feed_url_not_empty": "订阅源的网址不能为空。",
"error.fields_mandatory": "必须填写全部信息",
"error.http_bad_gateway": "当前由于错误的网关导致该网站无法访问,问题不在Miniflux,请稍后重试。",
"error.http_bad_gateway": "当前由于错误的网关导致该网站无法访问,问题不在 Miniflux,请稍后重试。",
"error.http_body_read": "无法读取HTTP主体: %v。",
"error.http_client_error": "HTTP 客户端错误r: %v。",
"error.http_empty_response": "HTTP响应内容为空,该网站可能正在使用机器人保护机制。",
"error.http_empty_response_body": "HTTP响应主体为空。",
"error.http_empty_response": "HTTP 响应内容为空,该网站可能正在使用机器人保护机制。",
"error.http_empty_response_body": "HTTP 响应主体为空。",
"error.http_forbidden": "该网站被禁止访问,网站可能有机器人保护机制?",
"error.http_gateway_timeout": "当前由于网关超时导致该网站无法访问,问题不在Miniflux,请稍后重试。",
"error.http_internal_server_error": "当前由于服务器错误导致该网站无法访问,问题不在Miniflux,请稍后重试。",
"error.http_gateway_timeout": "当前由于网关超时导致该网站无法访问,问题不在 Miniflux,请稍后重试。",
"error.http_internal_server_error": "当前由于服务器错误导致该网站无法访问,问题不在 Miniflux,请稍后重试。",
"error.http_not_authorized": "该网站访问未授权,可能用户名和密码错误。",
"error.http_resource_not_found": "请求资源无法找到,请检查URL。",
"error.http_response_too_large": "HTTP响应内容过大,您可以在全局设置中增加HTTP响应大小限制(需要服务器重新启动)。",
"error.http_service_unavailable": "当前由于服务器内部错误导致该网站无法访问,问题不在Miniflux,请稍后重试。",
"error.http_resource_not_found": "请求资源无法找到,请检查 URL。",
"error.http_response_too_large": "HTTP 响应内容过大,您可以在全局设置中增加 HTTP 响应大小限制(需要服务器重新启动)。",
"error.http_service_unavailable": "当前由于服务器内部错误导致该网站无法访问,问题不在 Miniflux,请稍后重试。",
"error.http_too_many_requests": "Miniflux 对该网站请求过多次数,请稍后重试或修改应用配置项。",
"error.http_unexpected_status_code": "当前由于意外的HTTP状态码:%d 导致该网站无法访问,问题不在Miniflux,请稍后重试。",
"error.http_unexpected_status_code": "当前由于意外的 HTTP 状态码:%d 导致该网站无法访问,问题不在 Miniflux,请稍后重试。",
"error.invalid_default_home_page": "无效的默认主页!",
"error.invalid_display_mode": "无效的网页应用显示模式。",
"error.invalid_entry_direction": "无效的输入方向。",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "订阅源的网址无效。",
"error.invalid_gesture_nav": "手势导航无效。",
"error.invalid_language": "无效的语言。",
"error.invalid_site_url": "源网站的网址无效。",
"error.invalid_theme": "无效的主题。",
"error.invalid_timezone": "无效的时区。",
"error.network_operation": "Miniflux无法访问该网站由于网络错误: %v。",
"error.network_operation": "Miniflux 无法访问该网站由于网络错误: %v。",
"error.network_timeout": "该网站响应过慢,请求超时: %v",
"error.password_min_length": "请至少输入 6 个字符",
"error.pocket_access_token": "无法从 Pocket 获取访问令牌!",
"error.pocket_request_token": "无法从 Pocket 获取请求令牌!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "无效的阻止规则: 规则 #%d 缺少合法的字段名 (可选: %s)",
"error.settings_block_rule_invalid_regex": "无效的阻止规则: 规则 #%d 的模式字符不是合法的正则表达式。",
"error.settings_block_rule_regex_required": "无效的阻止规则: 规则 #%d 的模式字符没有提供。",
@@ -141,11 +143,11 @@
"error.site_url_not_empty": "源网站的网址不能为空。",
"error.subscription_not_found": "找不到任何源",
"error.title_required": "必须填写标题",
"error.tls_error": "TLS 错误: %q。如果您愿意的话可以在订阅源设置里关闭TLS验证。",
"error.tls_error": "TLS 错误: %q。如果您愿意的话可以在订阅源设置里关闭 TLS 验证。",
"error.unable_to_create_api_key": "无法创建此 API 密钥。",
"error.unable_to_create_category": "无法建立这个分类",
"error.unable_to_create_user": "无法创建此用户",
"error.unable_to_detect_rssbridge": "无法使用RSS-Bridge去检测订阅源: %v。",
"error.unable_to_detect_rssbridge": "无法使用 RSS-Bridge 去检测订阅源: %v。",
"error.unable_to_parse_feed": "无法解析该订阅源: %v。",
"error.unable_to_update_category": "无法更新该分类",
"error.unable_to_update_feed": "无法更新此源",
@@ -153,7 +155,7 @@
"error.unlink_account_without_password": "您必须设置密码,否则您将无法再次登录。",
"error.user_already_exists": "用户已存在",
"error.user_mandatory_fields": "必须填写用户名",
"form.api_key.label.description": "API密钥标签",
"form.api_key.label.description": "API 密钥标签",
"form.category.hide_globally": "隐藏全局未读列表中的文章",
"form.category.label.title": "标题",
"form.feed.fieldset.general": "通用",
@@ -172,39 +174,41 @@
"form.feed.label.feed_password": "源密码",
"form.feed.label.feed_url": "订阅源 URL",
"form.feed.label.feed_username": "源用户名",
"form.feed.label.fetch_via_proxy": "通过代理获取",
"form.feed.label.fetch_via_proxy": "使用在应用程序级别配置的代理",
"form.feed.label.hide_globally": "隐藏全局未读列表中的文章",
"form.feed.label.ignore_http_cache": "忽略 HTTP 缓存",
"form.feed.label.keeplist_rules": "保留规则",
"form.feed.label.no_media_player": "没有媒体播放器(音频/视频)",
"form.feed.label.ntfy_activate": "推送条目到ntfy",
"form.feed.label.ntfy_default_priority": "Ntfy默认优先级",
"form.feed.label.ntfy_high_priority": "Ntfy高优先级",
"form.feed.label.ntfy_low_priority": "Ntfy低优先级",
"form.feed.label.ntfy_max_priority": "Ntfy最高优先级",
"form.feed.label.ntfy_min_priority": "Ntfy最低优先级",
"form.feed.label.ntfy_priority": "Ntfy优先级",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
"form.feed.label.pushover_low_priority": "Pushover low priority",
"form.feed.label.pushover_max_priority": "Pushover max priority",
"form.feed.label.pushover_min_priority": "Pushover min priority",
"form.feed.label.pushover_priority": "Pushover message priority",
"form.feed.label.ntfy_default_priority": "Ntfy 默认优先级",
"form.feed.label.ntfy_high_priority": "Ntfy 高优先级",
"form.feed.label.ntfy_low_priority": "Ntfy 低优先级",
"form.feed.label.ntfy_max_priority": "Ntfy 最高优先级",
"form.feed.label.ntfy_min_priority": "Ntfy 最低优先级",
"form.feed.label.ntfy_priority": "Ntfy 优先级",
"form.feed.label.ntfy_topic": "Ntfy 主题(可选)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "将条目推送至 pushover.net",
"form.feed.label.pushover_default_priority": "Pushover 默认优先级",
"form.feed.label.pushover_high_priority": "Pushover 高优先级",
"form.feed.label.pushover_low_priority": "Pushover 低优先级",
"form.feed.label.pushover_max_priority": "Pushover 最高优先级",
"form.feed.label.pushover_min_priority": "Pushover 最低优先级",
"form.feed.label.pushover_priority": "Pushover 消息优先级",
"form.feed.label.rewrite_rules": "重写规则",
"form.feed.label.scraper_rules": "抓取规则",
"form.feed.label.site_url": "源网站 URL",
"form.feed.label.title": "标题",
"form.feed.label.urlrewrite_rules": "URL 重写规则",
"form.feed.label.user_agent": "覆盖默认的用户代理",
"form.feed.label.webhook_url": "Override webhook url",
"form.feed.label.webhook_url": "覆盖 webhook url",
"form.import.label.file": "OPML 文件",
"form.import.label.url": "URL",
"form.integration.apprise_activate": "将新文章推送到 Apprise",
"form.integration.apprise_services_url": "使用逗号分隔的 Apprise 服务 URL 列表",
"form.integration.apprise_url": "Apprise API URL",
"form.integration.betula_activate": "保存文章到 Betula",
"form.integration.betula_token": "Betula 密钥",
"form.integration.betula_token": "Betula 令牌",
"form.integration.betula_url": "Betula 服务地址",
"form.integration.cubox_activate": "保存文章到 Cubox",
"form.integration.cubox_api_link": "Cubox API 链接",
@@ -246,15 +250,15 @@
"form.integration.matrix_bot_user": "Matrix Bot 用户名",
"form.integration.notion_activate": "保存文章到 Notion",
"form.integration.notion_page_id": "Notion 页面ID",
"form.integration.notion_token": "Notion 密钥",
"form.integration.ntfy_activate": "推送条目到ntfy",
"form.integration.notion_token": "Notion 令牌",
"form.integration.ntfy_activate": "推送条目到 ntfy",
"form.integration.ntfy_api_token": "Ntfy API令牌(可选)",
"form.integration.ntfy_icon_url": "Ntfy图标URL(可选)",
"form.integration.ntfy_internal_links": "Use internal links on click (optional)",
"form.integration.ntfy_password": "Ntfy密码(可选)",
"form.integration.ntfy_topic": "Ntfy主题",
"form.integration.ntfy_url": "Ntfy URL(可选,默认为ntfy.sh",
"form.integration.ntfy_username": "Ntfy用户名(可选)",
"form.integration.ntfy_icon_url": "Ntfy 图标 URL (可选)",
"form.integration.ntfy_internal_links": "点击时使用内部链接(可选)",
"form.integration.ntfy_password": "Ntfy 密码(可选)",
"form.integration.ntfy_topic": "Ntfy 主题(預設,如果未在此源中定義)",
"form.integration.ntfy_url": "Ntfy URL(可选,默认为 ntfy.sh",
"form.integration.ntfy_username": "Ntfy 用户名(可选)",
"form.integration.nunux_keeper_activate": "保存文章到 Nunux Keeper",
"form.integration.nunux_keeper_api_key": "Nunux Keeper API 密钥",
"form.integration.nunux_keeper_endpoint": "Nunux Keeper API 端点",
@@ -264,28 +268,28 @@
"form.integration.pinboard_activate": "保存文章到 Pinboard",
"form.integration.pinboard_bookmark": "标记为未读",
"form.integration.pinboard_tags": "Pinboard 标签",
"form.integration.pinboard_token": "Pinboard API 密钥",
"form.integration.pocket_access_token": "Pocket 访问密钥",
"form.integration.pinboard_token": "Pinboard API 令牌",
"form.integration.pocket_access_token": "Pocket 访问令牌",
"form.integration.pocket_activate": "将文章保存到 Pocket",
"form.integration.pocket_connect_link": "连接您的 Pocket 帐户",
"form.integration.pocket_consumer_key": "Pocket 用户密钥",
"form.integration.pushover_activate": "Push entries to Pushover",
"form.integration.pushover_device": "Pushover device (optional)",
"form.integration.pushover_prefix": "Pushover URL prefix (optional)",
"form.integration.pushover_token": "Pushover application API token",
"form.integration.pushover_user": "Pushover user key",
"form.integration.pushover_activate": "将条目推送至 Pushover",
"form.integration.pushover_device": "Pushover 装置(可选)",
"form.integration.pushover_prefix": "Pushover URL 前缀(可选)",
"form.integration.pushover_token": "Pushover 应用程序 API 令牌",
"form.integration.pushover_user": "Pushover 用户密钥",
"form.integration.raindrop_activate": "保存文章到 Raindrop",
"form.integration.raindrop_collection_id": "Collection ID",
"form.integration.raindrop_tags": "Tags (comma-separated)",
"form.integration.raindrop_token": "(Test) 密钥",
"form.integration.raindrop_collection_id": "集合 ID",
"form.integration.raindrop_tags": "Tags (逗号分隔)",
"form.integration.raindrop_token": "(Test) 令牌",
"form.integration.readeck_activate": "保存文章到 Readeck",
"form.integration.readeck_api_key": "Readeck API 密钥",
"form.integration.readeck_endpoint": "Readeck API 端点",
"form.integration.readeck_labels": "Readeck 默认标签",
"form.integration.readeck_only_url": "仅发送 URL(而不是完整内容)",
"form.integration.readwise_activate": "保存文章到 Readwise Reader",
"form.integration.readwise_api_key": "Readwise Reader 密钥",
"form.integration.readwise_api_key_link": "获取你的 Readwise 密钥",
"form.integration.readwise_api_key": "Readwise Reader 访问令牌",
"form.integration.readwise_api_key_link": "获取你的 Readwise 访问令牌",
"form.integration.rssbridge_activate": "添加订阅时检查 RSS-Bridge",
"form.integration.rssbridge_url": "RSS-Bridge 服务器 URL",
"form.integration.shaarli_activate": "保存文章到 Shaarli",
@@ -302,7 +306,7 @@
"form.integration.telegram_bot_disable_notification": "禁用通知",
"form.integration.telegram_bot_disable_web_page_preview": "禁用网页预览",
"form.integration.telegram_bot_token": "机器人令牌",
"form.integration.telegram_chat_id": "聊天ID",
"form.integration.telegram_chat_id": "聊天 ID",
"form.integration.telegram_topic_id": "Topic ID",
"form.integration.wallabag_activate": "保存文章到 Wallabag",
"form.integration.wallabag_client_id": "Wallabag 客户端 ID",
@@ -398,6 +402,7 @@
"page.about.author": "作者:",
"page.about.build_date": "构建日期:",
"page.about.credits": "版权",
"page.about.db_usage": "数据库容量",
"page.about.global_config_options": "全局配置选项",
"page.about.go_version": "Go 版本号:",
"page.about.license": "协议:",
@@ -415,7 +420,7 @@
"page.api_keys.table.created_at": "创建日期",
"page.api_keys.table.description": "描述",
"page.api_keys.table.last_used_at": "最后使用",
"page.api_keys.table.token": "密钥",
"page.api_keys.table.token": "令牌",
"page.api_keys.title": "API 密钥",
"page.categories_count": [
"%d 分类"
+9 -4
View File
@@ -69,11 +69,11 @@
"entry.shared_entry.title": "開啟公共連結",
"entry.state.loading": "載入中…",
"entry.state.saving": "儲存中…",
"entry.status.read": "標記為已讀",
"entry.status.mark_as_read": "標記為已讀",
"entry.status.mark_as_unread": "標記為未讀",
"entry.status.title": "更改狀態",
"entry.status.toast.read": "已標記為已讀",
"entry.status.toast.unread": "已標記為未讀",
"entry.status.unread": "標記為未讀",
"entry.tags.label": "標籤:",
"entry.unshare.label": "取消分享",
"error.api_key_already_exists": "此 API 金鑰已存在。",
@@ -115,6 +115,7 @@
"error.invalid_default_home_page": "預設主頁無效!",
"error.invalid_display_mode": "無效的顯示模式。",
"error.invalid_entry_direction": "無效的輸入方向。",
"error.invalid_feed_proxy_url": "Invalid proxy URL.",
"error.invalid_feed_url": "訂閱網址無效。",
"error.invalid_gesture_nav": "手勢導覽無效。",
"error.invalid_language": "無效的語言。",
@@ -126,6 +127,7 @@
"error.password_min_length": "請至少輸入 6 個字元",
"error.pocket_access_token": "無法從 Pocket 取得存取金鑰!",
"error.pocket_request_token": "無法從 Pocket 取得請求金鑰!",
"error.proxy_url_not_empty": "The proxy URL cannot be empty.",
"error.settings_block_rule_fieldname_invalid": "無效的封鎖規則:規則 #%d 缺少有效的欄位名稱 (可用選項:%s)",
"error.settings_block_rule_invalid_regex": "無效的封鎖規則:規則 #%d 的模式不是合法的正規表示式",
"error.settings_block_rule_regex_required": "無效的封鎖規則:規則 #%d 沒有提供正規表示式",
@@ -172,7 +174,7 @@
"form.feed.label.feed_password": "Feed 密碼",
"form.feed.label.feed_url": "Feed 網址",
"form.feed.label.feed_username": "Feed 使用者名稱",
"form.feed.label.fetch_via_proxy": "透過代理取得",
"form.feed.label.fetch_via_proxy": "使用應用程式層級設定的代理",
"form.feed.label.hide_globally": "在全域未讀列表中隱藏文章",
"form.feed.label.ignore_http_cache": "忽略 HTTP 快取",
"form.feed.label.keeplist_rules": "保留規則",
@@ -184,6 +186,8 @@
"form.feed.label.ntfy_max_priority": "Ntfy 最高優先順序",
"form.feed.label.ntfy_min_priority": "Ntfy 最低優先順序",
"form.feed.label.ntfy_priority": "Ntfy 優先順序",
"form.feed.label.ntfy_topic": "Ntfy topic (選填)",
"form.feed.label.proxy_url": "Proxy URL",
"form.feed.label.pushover_activate": "Push entries to pushover.net",
"form.feed.label.pushover_default_priority": "Pushover default priority",
"form.feed.label.pushover_high_priority": "Pushover high priority",
@@ -252,7 +256,7 @@
"form.integration.ntfy_icon_url": "Ntfy Icon 網址 (選填)",
"form.integration.ntfy_internal_links": "點選時使用內部連結 (選填)",
"form.integration.ntfy_password": "Ntfy 密碼 (選填)",
"form.integration.ntfy_topic": "Ntfy topic",
"form.integration.ntfy_topic": "Ntfy topic (飼料若無設定,則使用預設值)",
"form.integration.ntfy_url": "Ntfy 網址 (選填,預設為 ntfy.sh)",
"form.integration.ntfy_username": "Ntfy 使用者名稱 (選填)",
"form.integration.nunux_keeper_activate": "儲存文章到 Nunux Keeper",
@@ -398,6 +402,7 @@
"page.about.author": "作者:",
"page.about.build_date": "建構日期:",
"page.about.credits": "版權",
"page.about.db_usage": "Database size:",
"page.about.global_config_options": "全域設定選項",
"page.about.go_version": "Go 版本:",
"page.about.license": "授權:",
+15 -7
View File
@@ -19,16 +19,24 @@ func (c *Category) String() string {
return fmt.Sprintf("ID=%d, UserID=%d, Title=%s", c.ID, c.UserID, c.Title)
}
// CategoryRequest represents the request to create or update a category.
type CategoryRequest struct {
type CategoryCreationRequest struct {
Title string `json:"title"`
HideGlobally string `json:"hide_globally"`
HideGlobally bool `json:"hide_globally"`
}
// Patch updates category fields.
func (cr *CategoryRequest) Patch(category *Category) {
category.Title = cr.Title
category.HideGlobally = cr.HideGlobally != ""
type CategoryModificationRequest struct {
Title *string `json:"title"`
HideGlobally *bool `json:"hide_globally"`
}
func (c *CategoryModificationRequest) Patch(category *Category) {
if c.Title != nil {
category.Title = *c.Title
}
if c.HideGlobally != nil {
category.HideGlobally = *c.HideGlobally
}
}
// Categories represents a list of categories.
+21 -6
View File
@@ -56,8 +56,10 @@ type Feed struct {
WebhookURL string `json:"webhook_url"`
NtfyEnabled bool `json:"ntfy_enabled"`
NtfyPriority int `json:"ntfy_priority"`
PushoverEnabled bool `json:"pushover_enabled,omitempty"`
PushoverPriority int `json:"pushover_priority,omitempty"`
NtfyTopic string `json:"ntfy_topic"`
PushoverEnabled bool `json:"pushover_enabled"`
PushoverPriority int `json:"pushover_priority"`
ProxyURL string `json:"proxy_url"`
// Non-persisted attributes
Category *Category `json:"category,omitempty"`
@@ -115,9 +117,7 @@ func (f *Feed) CheckedNow() {
}
// ScheduleNextCheck set "next_check_at" of a feed based on the scheduler selected from the configuration.
func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelayInMinutes int) {
f.TTL = refreshDelayInMinutes
func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelayInMinutes int) int {
// Default to the global config Polling Frequency.
intervalMinutes := config.Opts.SchedulerRoundRobinMinInterval()
@@ -131,12 +131,21 @@ func (f *Feed) ScheduleNextCheck(weeklyCount int, refreshDelayInMinutes int) {
}
}
// If the feed has a TTL or a Retry-After defined, we use it to make sure we don't check it too often.
// Use the RSS TTL field, Retry-After, Cache-Control or Expires HTTP headers if defined.
if refreshDelayInMinutes > 0 && refreshDelayInMinutes > intervalMinutes {
intervalMinutes = refreshDelayInMinutes
}
// Limit the max interval value for misconfigured feeds.
switch config.Opts.PollingScheduler() {
case SchedulerRoundRobin:
intervalMinutes = min(intervalMinutes, config.Opts.SchedulerRoundRobinMaxInterval())
case SchedulerEntryFrequency:
intervalMinutes = min(intervalMinutes, config.Opts.SchedulerEntryFrequencyMaxInterval())
}
f.NextCheckAt = time.Now().Add(time.Minute * time.Duration(intervalMinutes))
return intervalMinutes
}
// FeedCreationRequest represents the request to create a feed.
@@ -160,6 +169,7 @@ type FeedCreationRequest struct {
HideGlobally bool `json:"hide_globally"`
UrlRewriteRules string `json:"urlrewrite_rules"`
DisableHTTP2 bool `json:"disable_http2"`
ProxyURL string `json:"proxy_url"`
}
type FeedCreationRequestFromSubscriptionDiscovery struct {
@@ -194,6 +204,7 @@ type FeedModificationRequest struct {
FetchViaProxy *bool `json:"fetch_via_proxy"`
HideGlobally *bool `json:"hide_globally"`
DisableHTTP2 *bool `json:"disable_http2"`
ProxyURL *string `json:"proxy_url"`
}
// Patch updates a feed with modified values.
@@ -285,6 +296,10 @@ func (f *FeedModificationRequest) Patch(feed *Feed) {
if f.DisableHTTP2 != nil {
feed.DisableHTTP2 = *f.DisableHTTP2
}
if f.ProxyURL != nil {
feed.ProxyURL = *f.ProxyURL
}
}
// Feeds is a list of feed
+23
View File
@@ -144,6 +144,29 @@ func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayBelowMinInterval(t *test
checkTargetInterval(t, feed, expectedInterval, timeBefore, "TestFeedScheduleNextCheckRoundRobinWithRefreshDelayBelowMinInterval")
}
func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMaxInterval(t *testing.T) {
os.Clearenv()
var err error
parser := config.NewParser()
config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
timeBefore := time.Now()
feed := &Feed{}
feed.ScheduleNextCheck(0, config.Opts.SchedulerRoundRobinMaxInterval()+30)
if feed.NextCheckAt.IsZero() {
t.Error(`The next_check_at must be set`)
}
expectedInterval := config.Opts.SchedulerRoundRobinMaxInterval()
checkTargetInterval(t, feed, expectedInterval, timeBefore, "TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMaxInterval")
}
func TestFeedScheduleNextCheckRoundRobinMinInterval(t *testing.T) {
minInterval := 1
os.Clearenv()
+8 -6
View File
@@ -10,10 +10,11 @@ import (
// Icon represents a website icon (favicon)
type Icon struct {
ID int64 `json:"id"`
Hash string `json:"hash"`
MimeType string `json:"mime_type"`
Content []byte `json:"-"`
ID int64 `json:"id"`
Hash string `json:"hash"`
MimeType string `json:"mime_type"`
Content []byte `json:"-"`
ExternalID string `json:"external_id"`
}
// DataURL returns the data URL of the icon.
@@ -26,6 +27,7 @@ type Icons []*Icon
// FeedIcon is a junction table between feeds and icons.
type FeedIcon struct {
FeedID int64 `json:"feed_id"`
IconID int64 `json:"icon_id"`
FeedID int64 `json:"feed_id"`
IconID int64 `json:"icon_id"`
ExternalIconID string `json:"external_icon_id"`
}
+4
View File
@@ -20,3 +20,7 @@ func OptionalString(value string) *string {
}
return nil
}
func SetOptionalField[T any](value T) *T {
return &value
}
+1
View File
@@ -10,6 +10,7 @@ type SubscriptionDiscoveryRequest struct {
Cookie string `json:"cookie"`
Username string `json:"username"`
Password string `json:"password"`
ProxyURL string `json:"proxy_url"`
FetchViaProxy bool `json:"fetch_via_proxy"`
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
DisableHTTP2 bool `json:"disable_http2"`
+1 -1
View File
@@ -36,7 +36,7 @@ func (s WebAuthnSession) String() string {
if s.SessionData == nil {
return "{}"
}
return fmt.Sprintf("{Challenge: %s, UserID: %x}", s.SessionData.Challenge, s.SessionData.UserID)
return fmt.Sprintf("{Challenge: %s, UserID: %x}", s.Challenge, s.UserID)
}
type WebAuthnCredential struct {
+4
View File
@@ -39,5 +39,9 @@ func NewManager(ctx context.Context, clientID, clientSecret, redirectURL, oidcDi
}
}
if clientSecret == "" {
slog.Warn("OIDC client secret is empty or missing.")
}
return m
}
+60
View File
@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package proxyrotator // import "miniflux.app/v2/internal/proxyrotator"
import (
"net/url"
"sync"
)
var ProxyRotatorInstance *ProxyRotator
// ProxyRotator manages a list of proxies and rotates through them.
type ProxyRotator struct {
proxies []*url.URL
currentIndex int
mutex sync.Mutex
}
// NewProxyRotator creates a new ProxyRotator with the given proxy URLs.
func NewProxyRotator(proxyURLs []string) (*ProxyRotator, error) {
parsedProxies := make([]*url.URL, 0, len(proxyURLs))
for _, p := range proxyURLs {
proxyURL, err := url.Parse(p)
if err != nil {
return nil, err
}
parsedProxies = append(parsedProxies, proxyURL)
}
return &ProxyRotator{
proxies: parsedProxies,
currentIndex: 0,
mutex: sync.Mutex{},
}, nil
}
// GetNextProxy returns the next proxy in the rotation.
func (pr *ProxyRotator) GetNextProxy() *url.URL {
pr.mutex.Lock()
defer pr.mutex.Unlock()
if len(pr.proxies) == 0 {
return nil
}
proxy := pr.proxies[pr.currentIndex]
pr.currentIndex = (pr.currentIndex + 1) % len(pr.proxies)
return proxy
}
// HasProxies checks if there are any proxies available in the rotator.
func (pr *ProxyRotator) HasProxies() bool {
pr.mutex.Lock()
defer pr.mutex.Unlock()
return len(pr.proxies) > 0
}
@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package proxyrotator // import "miniflux.app/v2/internal/proxyrotator"
import (
"testing"
)
func TestProxyRotator(t *testing.T) {
proxyURLs := []string{
"http://proxy1.example.com",
"http://proxy2.example.com",
"http://proxy3.example.com",
}
rotator, err := NewProxyRotator(proxyURLs)
if err != nil {
t.Fatalf("Failed to create ProxyRotator: %v", err)
}
if !rotator.HasProxies() {
t.Fatalf("Expected rotator to have proxies")
}
seenProxies := make(map[string]bool)
for range len(proxyURLs) * 2 {
proxy := rotator.GetNextProxy()
if proxy == nil {
t.Fatalf("Expected a proxy, got nil")
}
seenProxies[proxy.String()] = true
}
if len(seenProxies) != len(proxyURLs) {
t.Fatalf("Expected to see all proxies, but saw: %v", seenProxies)
}
}
func TestProxyRotatorEmpty(t *testing.T) {
rotator, err := NewProxyRotator([]string{})
if err != nil {
t.Fatalf("Failed to create ProxyRotator: %v", err)
}
if rotator.HasProxies() {
t.Fatalf("Expected rotator to have no proxies")
}
proxy := rotator.GetNextProxy()
if proxy != nil {
t.Fatalf("Expected no proxy, got: %v", proxy)
}
}
func TestProxyRotatorInvalidURL(t *testing.T) {
invalidProxyURLs := []string{
"http://validproxy.example.com",
"test|test://invalidproxy.example.com",
}
rotator, err := NewProxyRotator(invalidProxyURLs)
if err == nil {
t.Fatalf("Expected an error when creating ProxyRotator with invalid URLs, but got none")
}
if rotator != nil {
t.Fatalf("Expected rotator to be nil when initialization fails, but got: %v", rotator)
}
}
+4 -4
View File
@@ -116,12 +116,12 @@ type Atom03Content struct {
func (a *Atom03Content) Content() string {
content := ""
switch {
case a.Mode == "xml":
switch a.Mode {
case "xml":
content = a.InnerXML
case a.Mode == "escaped":
case "escaped":
content = a.CharData
case a.Mode == "base64":
case "base64":
b, err := base64.StdEncoding.DecodeString(a.CharData)
if err == nil {
content = string(b)
+38 -13
View File
@@ -6,11 +6,14 @@ package fetcher // import "miniflux.app/v2/internal/reader/fetcher"
import (
"crypto/tls"
"encoding/base64"
"fmt"
"log/slog"
"net"
"net/http"
"net/url"
"time"
"miniflux.app/v2/internal/proxyrotator"
)
const (
@@ -21,12 +24,14 @@ const (
type RequestBuilder struct {
headers http.Header
clientProxyURL string
clientProxyURL *url.URL
useClientProxy bool
clientTimeout int
withoutRedirects bool
ignoreTLSErrors bool
disableHTTP2 bool
proxyRotator *proxyrotator.ProxyRotator
feedProxyURL string
}
func NewRequestBuilder() *RequestBuilder {
@@ -78,16 +83,26 @@ func (r *RequestBuilder) WithUsernameAndPassword(username, password string) *Req
return r
}
func (r *RequestBuilder) WithProxy(proxyURL string) *RequestBuilder {
func (r *RequestBuilder) WithProxyRotator(proxyRotator *proxyrotator.ProxyRotator) *RequestBuilder {
r.proxyRotator = proxyRotator
return r
}
func (r *RequestBuilder) WithCustomApplicationProxyURL(proxyURL *url.URL) *RequestBuilder {
r.clientProxyURL = proxyURL
return r
}
func (r *RequestBuilder) UseProxy(value bool) *RequestBuilder {
func (r *RequestBuilder) UseCustomApplicationProxyURL(value bool) *RequestBuilder {
r.useClientProxy = value
return r
}
func (r *RequestBuilder) WithCustomFeedProxyURL(proxyURL string) *RequestBuilder {
r.feedProxyURL = proxyURL
return r
}
func (r *RequestBuilder) WithTimeout(timeout int) *RequestBuilder {
r.clientTimeout = timeout
return r
@@ -151,15 +166,25 @@ func (r *RequestBuilder) ExecuteRequest(requestURL string) (*http.Response, erro
transport.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
}
if r.useClientProxy && r.clientProxyURL != "" {
if proxyURL, err := url.Parse(r.clientProxyURL); err != nil {
slog.Warn("Unable to parse proxy URL",
slog.String("proxy_url", r.clientProxyURL),
slog.Any("error", err),
)
} else {
transport.Proxy = http.ProxyURL(proxyURL)
var clientProxyURL *url.URL
switch {
case r.feedProxyURL != "":
var err error
clientProxyURL, err = url.Parse(r.feedProxyURL)
if err != nil {
return nil, fmt.Errorf(`fetcher: invalid feed proxy URL %q: %w`, r.feedProxyURL, err)
}
case r.useClientProxy && r.clientProxyURL != nil:
clientProxyURL = r.clientProxyURL
case r.proxyRotator != nil && r.proxyRotator.HasProxies():
clientProxyURL = r.proxyRotator.GetNextProxy()
}
var clientProxyURLRedacted string
if clientProxyURL != nil {
transport.Proxy = http.ProxyURL(clientProxyURL)
clientProxyURLRedacted = clientProxyURL.Redacted()
}
client := &http.Client{
@@ -189,8 +214,8 @@ func (r *RequestBuilder) ExecuteRequest(requestURL string) (*http.Response, erro
slog.String("url", req.URL.String()),
slog.Any("headers", req.Header),
slog.Bool("without_redirects", r.withoutRedirects),
slog.Bool("with_proxy", r.useClientProxy),
slog.String("proxy_url", r.clientProxyURL),
slog.Bool("use_app_client_proxy", r.useClientProxy),
slog.String("client_proxy_url", clientProxyURLRedacted),
slog.Bool("ignore_tls_errors", r.ignoreTLSErrors),
slog.Bool("disable_http2", r.disableHTTP2),
))
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"log/slog"
"math"
"net"
"net/http"
"net/url"
@@ -53,6 +54,33 @@ func (r *ResponseHandler) ETag() string {
return r.httpResponse.Header.Get("ETag")
}
func (r *ResponseHandler) ExpiresInMinutes() int {
expiresHeaderValue := r.httpResponse.Header.Get("Expires")
if expiresHeaderValue != "" {
t, err := time.Parse(time.RFC1123, expiresHeaderValue)
if err == nil {
return int(math.Ceil(time.Until(t).Minutes()))
}
}
return 0
}
func (r *ResponseHandler) CacheControlMaxAgeInMinutes() int {
cacheControlHeaderValue := r.httpResponse.Header.Get("Cache-Control")
if cacheControlHeaderValue != "" {
for _, directive := range strings.Split(cacheControlHeaderValue, ",") {
directive = strings.TrimSpace(directive)
if strings.HasPrefix(directive, "max-age=") {
maxAge, err := strconv.Atoi(strings.TrimPrefix(directive, "max-age="))
if err == nil {
return int(math.Ceil(float64(maxAge) / 60))
}
}
}
}
return 0
}
func (r *ResponseHandler) ParseRetryDelay() int {
retryAfterHeaderValue := r.httpResponse.Header.Get("Retry-After")
if retryAfterHeaderValue != "" {
@@ -102,3 +102,75 @@ func TestRetryDelay(t *testing.T) {
})
}
}
func TestExpiresInMinutes(t *testing.T) {
var testCases = map[string]struct {
ExpiresHeader string
ExpectedMinutes int
}{
"Empty header": {
ExpiresHeader: "",
ExpectedMinutes: 0,
},
"Valid Expires header": {
ExpiresHeader: time.Now().Add(10 * time.Minute).Format(time.RFC1123),
ExpectedMinutes: 10,
},
"Invalid Expires header": {
ExpiresHeader: "invalid-date",
ExpectedMinutes: 0,
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
header := http.Header{}
header.Add("Expires", tc.ExpiresHeader)
rh := ResponseHandler{
httpResponse: &http.Response{
Header: header,
},
}
if tc.ExpectedMinutes != rh.ExpiresInMinutes() {
t.Errorf("Expected %d, got %d for scenario %q", tc.ExpectedMinutes, rh.ExpiresInMinutes(), name)
}
})
}
}
func TestCacheControlMaxAgeInMinutes(t *testing.T) {
var testCases = map[string]struct {
CacheControlHeader string
ExpectedMinutes int
}{
"Empty header": {
CacheControlHeader: "",
ExpectedMinutes: 0,
},
"Valid max-age": {
CacheControlHeader: "max-age=600",
ExpectedMinutes: 10,
},
"Invalid max-age": {
CacheControlHeader: "max-age=invalid",
ExpectedMinutes: 0,
},
"Multiple directives": {
CacheControlHeader: "no-cache, max-age=300",
ExpectedMinutes: 5,
},
}
for name, tc := range testCases {
t.Run(name, func(tt *testing.T) {
header := http.Header{}
header.Add("Cache-Control", tc.CacheControlHeader)
rh := ResponseHandler{
httpResponse: &http.Response{
Header: header,
},
}
if tc.ExpectedMinutes != rh.CacheControlMaxAgeInMinutes() {
t.Errorf("Expected %d, got %d for scenario %q", tc.ExpectedMinutes, rh.CacheControlMaxAgeInMinutes(), name)
}
})
}
}
+37 -11
View File
@@ -12,6 +12,7 @@ import (
"miniflux.app/v2/internal/integration"
"miniflux.app/v2/internal/locale"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
"miniflux.app/v2/internal/reader/icon"
"miniflux.app/v2/internal/reader/parser"
@@ -29,6 +30,7 @@ func CreateFeedFromSubscriptionDiscovery(store *storage.Storage, userID int64, f
slog.Debug("Begin feed creation process from subscription discovery",
slog.Int64("user_id", userID),
slog.String("feed_url", feedCreationRequest.FeedURL),
slog.String("proxy_url", feedCreationRequest.ProxyURL),
)
if !store.CategoryIDExists(userID, feedCreationRequest.CategoryID) {
@@ -64,6 +66,7 @@ func CreateFeedFromSubscriptionDiscovery(store *storage.Storage, userID int64, f
subscription.FeedURL = feedCreationRequest.FeedURL
subscription.DisableHTTP2 = feedCreationRequest.DisableHTTP2
subscription.WithCategoryID(feedCreationRequest.CategoryID)
subscription.ProxyURL = feedCreationRequest.ProxyURL
subscription.CheckedNow()
processor.ProcessFeedEntries(store, subscription, userID, true)
@@ -83,8 +86,10 @@ func CreateFeedFromSubscriptionDiscovery(store *storage.Storage, userID int64, f
requestBuilder.WithUserAgent(feedCreationRequest.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(feedCreationRequest.Cookie)
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.UseProxy(feedCreationRequest.FetchViaProxy)
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(feedCreationRequest.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(feedCreationRequest.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(feedCreationRequest.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(feedCreationRequest.DisableHTTP2)
@@ -98,6 +103,7 @@ func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model
slog.Debug("Begin feed creation process",
slog.Int64("user_id", userID),
slog.String("feed_url", feedCreationRequest.FeedURL),
slog.String("proxy_url", feedCreationRequest.ProxyURL),
)
if !store.CategoryIDExists(userID, feedCreationRequest.CategoryID) {
@@ -109,8 +115,10 @@ func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model
requestBuilder.WithUserAgent(feedCreationRequest.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(feedCreationRequest.Cookie)
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.UseProxy(feedCreationRequest.FetchViaProxy)
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(feedCreationRequest.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(feedCreationRequest.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(feedCreationRequest.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(feedCreationRequest.DisableHTTP2)
@@ -157,6 +165,7 @@ func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model
subscription.EtagHeader = responseHandler.ETag()
subscription.LastModifiedHeader = responseHandler.LastModified()
subscription.FeedURL = responseHandler.EffectiveURL()
subscription.ProxyURL = feedCreationRequest.ProxyURL
subscription.WithCategoryID(feedCreationRequest.CategoryID)
subscription.CheckedNow()
@@ -212,8 +221,10 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
requestBuilder.WithUserAgent(originalFeed.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(originalFeed.Cookie)
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.UseProxy(originalFeed.FetchViaProxy)
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(originalFeed.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(originalFeed.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(originalFeed.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(originalFeed.DisableHTTP2)
@@ -229,18 +240,24 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
if responseHandler.IsRateLimited() {
retryDelayInSeconds := responseHandler.ParseRetryDelay()
refreshDelayInMinutes = retryDelayInSeconds / 60
originalFeed.ScheduleNextCheck(weeklyEntryCount, refreshDelayInMinutes)
calculatedNextCheckIntervalInMinutes := originalFeed.ScheduleNextCheck(weeklyEntryCount, refreshDelayInMinutes)
slog.Warn("Feed is rate limited",
slog.String("feed_url", originalFeed.FeedURL),
slog.Int("retry_delay_in_seconds", retryDelayInSeconds),
slog.Int("refresh_delay_in_minutes", refreshDelayInMinutes),
slog.Int("calculated_next_check_interval_in_minutes", calculatedNextCheckIntervalInMinutes),
slog.Time("new_next_check_at", originalFeed.NextCheckAt),
)
}
if localizedError := responseHandler.LocalizedError(); localizedError != nil {
slog.Warn("Unable to fetch feed", slog.String("feed_url", originalFeed.FeedURL), slog.Any("error", localizedError.Error()))
slog.Warn("Unable to fetch feed",
slog.Int64("user_id", userID),
slog.Int64("feed_id", feedID),
slog.String("feed_url", originalFeed.FeedURL),
slog.Any("error", localizedError.Error()),
)
user, storeErr := store.UserByID(userID)
if storeErr != nil {
return locale.NewLocalizedErrorWrapper(storeErr, "error.database_error", storeErr)
@@ -292,16 +309,25 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
return localizedError
}
// If the feed has a TTL defined, we use it to make sure we don't check it too often.
refreshDelayInMinutes = updatedFeed.TTL
// Use the RSS TTL value, or the Cache-Control or Expires HTTP headers if available.
// Otherwise, we use the default value from the configuration (min interval parameter).
feedTTLValue := updatedFeed.TTL
cacheControlMaxAgeValue := responseHandler.CacheControlMaxAgeInMinutes()
expiresValue := responseHandler.ExpiresInMinutes()
refreshDelayInMinutes = max(feedTTLValue, cacheControlMaxAgeValue, expiresValue)
// Set the next check at with updated arguments.
originalFeed.ScheduleNextCheck(weeklyEntryCount, refreshDelayInMinutes)
calculatedNextCheckIntervalInMinutes := originalFeed.ScheduleNextCheck(weeklyEntryCount, refreshDelayInMinutes)
slog.Debug("Updated next check date",
slog.Int64("user_id", userID),
slog.Int64("feed_id", feedID),
slog.String("feed_url", originalFeed.FeedURL),
slog.Int("feed_ttl_minutes", feedTTLValue),
slog.Int("cache_control_max_age_in_minutes", cacheControlMaxAgeValue),
slog.Int("expires_in_minutes", expiresValue),
slog.Int("refresh_delay_in_minutes", refreshDelayInMinutes),
slog.Int("calculated_next_check_interval_in_minutes", calculatedNextCheckIntervalInMinutes),
slog.Time("new_next_check_at", originalFeed.NextCheckAt),
)
+5 -2
View File
@@ -8,6 +8,7 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
"miniflux.app/v2/internal/storage"
)
@@ -29,8 +30,10 @@ func (c *IconChecker) fetchAndStoreIcon() {
requestBuilder.WithUserAgent(c.feed.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(c.feed.Cookie)
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.UseProxy(c.feed.FetchViaProxy)
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(c.feed.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(c.feed.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(c.feed.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(c.feed.DisableHTTP2)
+4 -2
View File
@@ -61,7 +61,7 @@ func (h *Handler) Import(userID int64, data io.Reader) error {
}
if category == nil {
category, err = h.store.CreateCategory(userID, &model.CategoryRequest{Title: subscription.CategoryName})
category, err = h.store.CreateCategory(userID, &model.CategoryCreationRequest{Title: subscription.CategoryName})
if err != nil {
return fmt.Errorf(`opml: unable to create this category: %q`, subscription.CategoryName)
}
@@ -77,7 +77,9 @@ func (h *Handler) Import(userID int64, data io.Reader) error {
Category: category,
}
h.store.CreateFeed(feed)
if err := h.store.CreateFeed(feed); err != nil {
return fmt.Errorf(`opml: unable to create this feed: %q`, subscription.FeedURL)
}
}
}
+3 -2
View File
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package processor // import "miniflux.app/v2/internal/reader/processor
package processor // import "miniflux.app/v2/internal/reader/processor"
import (
"encoding/json"
@@ -11,6 +11,7 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
)
@@ -45,7 +46,7 @@ func extractBilibiliVideoID(websiteURL string) (string, string, error) {
func fetchBilibiliWatchTime(websiteURL string) (int, error) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
idType, videoID, extractErr := extractBilibiliVideoID(websiteURL)
if extractErr != nil {
+1 -1
View File
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package processor // import "miniflux.app/v2/internal/reader/processor
package processor // import "miniflux.app/v2/internal/reader/processor"
import (
"log/slog"
+3 -2
View File
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package processor // import "miniflux.app/v2/internal/reader/processor
package processor // import "miniflux.app/v2/internal/reader/processor"
import (
"errors"
@@ -14,6 +14,7 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
)
@@ -33,7 +34,7 @@ func shouldFetchNebulaWatchTime(entry *model.Entry) bool {
func fetchNebulaWatchTime(websiteURL string) (int, error) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(websiteURL))
defer responseHandler.Close()
+3 -2
View File
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package processor // import "miniflux.app/v2/internal/reader/processor
package processor // import "miniflux.app/v2/internal/reader/processor"
import (
"errors"
@@ -14,6 +14,7 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
)
@@ -33,7 +34,7 @@ func shouldFetchOdyseeWatchTime(entry *model.Entry) bool {
func fetchOdyseeWatchTime(websiteURL string) (int, error) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(websiteURL))
defer responseHandler.Close()
+10 -5
View File
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package processor // import "miniflux.app/v2/internal/reader/processor
package processor // import "miniflux.app/v2/internal/reader/processor"
import (
"log/slog"
@@ -14,6 +14,7 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/metric"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
"miniflux.app/v2/internal/reader/readingtime"
"miniflux.app/v2/internal/reader/rewrite"
@@ -78,8 +79,10 @@ func ProcessFeedEntries(store *storage.Storage, feed *model.Feed, userID int64,
requestBuilder.WithUserAgent(feed.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(feed.Cookie)
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.UseProxy(feed.FetchViaProxy)
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(feed.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(feed.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(feed.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(feed.DisableHTTP2)
@@ -145,8 +148,10 @@ func ProcessEntryWebPage(feed *model.Feed, entry *model.Entry, user *model.User)
requestBuilder.WithUserAgent(feed.UserAgent, config.Opts.HTTPClientUserAgent())
requestBuilder.WithCookie(feed.Cookie)
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.UseProxy(feed.FetchViaProxy)
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
requestBuilder.WithCustomFeedProxyURL(feed.ProxyURL)
requestBuilder.WithCustomApplicationProxyURL(config.Opts.HTTPClientProxyURL())
requestBuilder.UseCustomApplicationProxyURL(feed.FetchViaProxy)
requestBuilder.IgnoreTLSErrors(feed.AllowSelfSignedCertificates)
requestBuilder.DisableHTTP2(feed.DisableHTTP2)
+1 -1
View File
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package processor // import "miniflux.app/v2/internal/reader/processor
package processor // import "miniflux.app/v2/internal/reader/processor"
import (
"log/slog"
+4 -3
View File
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package processor // import "miniflux.app/v2/internal/reader/processor
package processor // import "miniflux.app/v2/internal/reader/processor"
import (
"encoding/json"
@@ -18,6 +18,7 @@ import (
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
"miniflux.app/v2/internal/proxyrotator"
"miniflux.app/v2/internal/reader/fetcher"
)
@@ -52,7 +53,7 @@ func fetchYouTubeWatchTimeForSingleEntry(websiteURL string) (int, error) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(websiteURL))
defer responseHandler.Close()
@@ -132,7 +133,7 @@ func fetchYouTubeWatchTimeFromApiInBulk(videoIDs []string) (map[string]time.Dura
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(apiURL.String()))
defer responseHandler.Close()
@@ -33,11 +33,6 @@ func TestGetRefererForURL(t *testing.T) {
url: "https://scontent-sjc3-1.cdninstagram.com/example.jpg",
expected: "https://www.instagram.com",
},
{
name: "Piokok URL",
url: "https://sp1.piokok.com/example.jpg",
expected: "https://sp1.piokok.com",
},
{
name: "Weibo Video URL",
url: "https://f.video.weibocdn.com/example.mp4",
+10 -13
View File
@@ -36,7 +36,6 @@ var predefinedRules = map[string]string{
"thedoghousediaries.com": "add_image_title",
"theverge.com": `add_dynamic_image, remove("div.duet--recirculation--related-list, .hidden")`,
"treelobsters.com": "add_image_title",
"webtoons.com": `add_dynamic_image,replace("webtoon"|"swebtoon")`,
"www.qwantz.com": "add_image_title,add_mailto_subject",
"xkcd.com": "add_image_title",
"youtube.com": "add_youtube_video",
@@ -50,31 +49,29 @@ func GetRefererForURL(u string) string {
}
switch parsedUrl.Hostname() {
case "moyu.im":
return "https://i.jandan.net"
case "i.pximg.net":
return "https://www.pixiv.net"
case "sp1.piokok.com":
return "https://sp1.piokok.com"
case "appinn.com":
return "https://appinn.com"
case "bjp.org.cn":
return "https://bjp.org.cn"
case "cdnfile.sspai.com":
return "https://sspai.com"
case "f.video.weibocdn.com":
return "https://weibo.com"
case "i.pximg.net":
return "https://www.pixiv.net"
case "img.hellogithub.com":
return "https://hellogithub.com"
case "bjp.org.cn":
return "https://bjp.org.cn"
case "appinn.com":
return "https://appinn.com"
case "moyu.im":
return "https://i.jandan.net"
}
switch {
case strings.HasSuffix(parsedUrl.Hostname(), ".sinaimg.cn"):
return "https://weibo.com"
case strings.HasSuffix(parsedUrl.Hostname(), ".cdninstagram.com"):
return "https://www.instagram.com"
case strings.HasSuffix(parsedUrl.Hostname(), ".moyu.im"):
return "https://i.jandan.net"
case strings.HasSuffix(parsedUrl.Hostname(), ".sinaimg.cn"):
return "https://weibo.com"
}
return ""
+4 -4
View File
@@ -39,7 +39,7 @@ func (r *RSSAdapter) BuildFeed(baseURL string) *model.Feed {
}
// Try to find the feed URL from the Atom links.
for _, atomLink := range r.rss.Channel.AtomLinks.Links {
for _, atomLink := range r.rss.Channel.Links {
atomLinkHref := strings.TrimSpace(atomLink.Href)
if atomLinkHref != "" && atomLink.Rel == "self" {
if absoluteFeedURL, err := urllib.AbsoluteURL(feed.FeedURL, atomLinkHref); err == nil {
@@ -189,7 +189,7 @@ func findEntryURL(rssItem *RSSItem) string {
}
}
for _, atomLink := range rssItem.AtomLinks.Links {
for _, atomLink := range rssItem.Links {
if atomLink.Href != "" && (strings.EqualFold(atomLink.Rel, "alternate") || atomLink.Rel == "") {
return strings.TrimSpace(atomLink.Href)
}
@@ -253,8 +253,8 @@ func findEntryAuthor(rssItem *RSSItem) string {
author = rssItem.ItunesAuthor
case rssItem.DublinCoreCreator != "":
author = rssItem.DublinCoreCreator
case rssItem.AtomAuthor.PersonName() != "":
author = rssItem.AtomAuthor.PersonName()
case rssItem.PersonName() != "":
author = rssItem.PersonName()
case strings.Contains(rssItem.Author.Inner, "<![CDATA["):
author = rssItem.Author.Data
default:
+5 -2
View File
@@ -25,6 +25,7 @@ var (
"aside": {},
"audio": {"src"},
"blockquote": {},
"b": {},
"br": {},
"caption": {},
"cite": {},
@@ -71,6 +72,7 @@ var (
"thead": {},
"time": {"datetime"},
"tr": {},
"u": {},
"ul": {"id"},
"var": {},
"video": {"poster", "height", "width", "src"},
@@ -281,9 +283,10 @@ func isPixelTracker(tagName string, attributes []html.Attribute) bool {
for _, attribute := range attributes {
if attribute.Val == "1" {
if attribute.Key == "height" {
switch attribute.Key {
case "height":
hasHeight = true
} else if attribute.Key == "width" {
case "width":
hasWidth = true
}
}
+2 -2
View File
@@ -198,7 +198,7 @@ func TestProtocolRelativeURL(t *testing.T) {
}
func TestInvalidTag(t *testing.T) {
input := `<p>My invalid <b>tag</b>.</p>`
input := `<p>My invalid <z>tag</z>.</p>`
expected := `<p>My invalid tag.</p>`
output := Sanitize("http://example.org/", input)
@@ -238,7 +238,7 @@ func TestUnknownTag(t *testing.T) {
}
func TestInvalidNestedTag(t *testing.T) {
input := `<p>My invalid <b>tag with some <em>valid</em> tag</b>.</p>`
input := `<p>My invalid <z>tag with some <em>valid</em> tag</z>.</p>`
expected := `<p>My invalid tag with some <em>valid</em> tag.</p>`
output := Sanitize("http://example.org/", input)
+3 -3
View File
@@ -49,10 +49,10 @@ func parseImageCandidate(input string) (*ImageCandidate, error) {
parts := strings.Split(strings.TrimSpace(input), " ")
nbParts := len(parts)
switch {
case nbParts == 1:
switch nbParts {
case 1:
return &ImageCandidate{ImageURL: parts[0]}, nil
case nbParts == 2:
case 2:
if !isValidWidthOrDensityDescriptor(parts[1]) {
return nil, fmt.Errorf(`srcset: invalid descriptor`)
}
+9 -8
View File
@@ -189,14 +189,15 @@ func (f *SubscriptionFinder) FindSubscriptionsFromWebPage(websiteURL, contentTyp
func (f *SubscriptionFinder) FindSubscriptionsFromWellKnownURLs(websiteURL string) (Subscriptions, *locale.LocalizedErrorWrapper) {
knownURLs := map[string]string{
"atom.xml": parser.FormatAtom,
"feed.xml": parser.FormatAtom,
"feed/": parser.FormatAtom,
"rss.xml": parser.FormatRSS,
"rss/": parser.FormatRSS,
"index.rss": parser.FormatRSS,
"index.xml": parser.FormatRSS,
"feed.atom": parser.FormatAtom,
"atom.xml": parser.FormatAtom,
"feed.atom": parser.FormatAtom,
"feed.xml": parser.FormatAtom,
"feed/": parser.FormatAtom,
"index.rss": parser.FormatRSS,
"index.xml": parser.FormatRSS,
"rss.xml": parser.FormatRSS,
"rss/": parser.FormatRSS,
"rss/feed.xml": parser.FormatRSS,
}
websiteURLRoot := urllib.RootURL(websiteURL)
+9
View File
@@ -14,6 +14,7 @@ import (
// https://firefox.settings.services.mozilla.com/v1/buckets/main/collections/query-stripping/records
// https://github.com/Smile4ever/Neat-URL/blob/master/data/default-params-by-category.json
// https://github.com/brave/brave-core/blob/master/components/query_filter/utils.cc
// https://developers.google.com/analytics/devguides/collection/ga4/reference/config
var trackingParams = map[string]bool{
// Facebook Click Identifiers
"fbclid": true,
@@ -31,6 +32,14 @@ var trackingParams = map[string]bool{
"wbraid": true,
"gclsrc": true,
// Google Analytics
"campaign_id": true,
"campaign_medium": true,
"campaign_name": true,
"campaign_source": true,
"campaign_term": true,
"campaign_content": true,
// Yandex Click Identifiers
"yclid": true,
"ysclid": true,
+8 -5
View File
@@ -165,27 +165,30 @@ func (s *Storage) CategoriesWithFeedCount(userID int64) (model.Categories, error
}
// CreateCategory creates a new category.
func (s *Storage) CreateCategory(userID int64, request *model.CategoryRequest) (*model.Category, error) {
func (s *Storage) CreateCategory(userID int64, request *model.CategoryCreationRequest) (*model.Category, error) {
var category model.Category
query := `
INSERT INTO categories
(user_id, title)
(user_id, title, hide_globally)
VALUES
($1, $2)
($1, $2, $3)
RETURNING
id,
user_id,
title
title,
hide_globally
`
err := s.db.QueryRow(
query,
userID,
request.Title,
request.HideGlobally,
).Scan(
&category.ID,
&category.UserID,
&category.Title,
&category.HideGlobally,
)
if err != nil {
@@ -197,7 +200,7 @@ func (s *Storage) CreateCategory(userID int64, request *model.CategoryRequest) (
// UpdateCategory updates an existing category.
func (s *Storage) UpdateCategory(category *model.Category) error {
query := `UPDATE categories SET title=$1, hide_globally = $2 WHERE id=$3 AND user_id=$4`
query := `UPDATE categories SET title=$1, hide_globally=$2 WHERE id=$3 AND user_id=$4`
_, err := s.db.Exec(
query,
category.Title,
+45
View File
@@ -60,6 +60,51 @@ func (s *Storage) GetEnclosures(entryID int64) (model.EnclosureList, error) {
return enclosures, nil
}
func (s *Storage) GetEnclosuresForEntries(entryIDs []int64) (map[int64]model.EnclosureList, error) {
query := `
SELECT
id,
user_id,
entry_id,
url,
size,
mime_type,
media_progression
FROM
enclosures
WHERE
entry_id = ANY($1)
ORDER BY id ASC
`
rows, err := s.db.Query(query, pq.Array(entryIDs))
if err != nil {
return nil, fmt.Errorf("store: unable to fetch enclosures: %w", err)
}
defer rows.Close()
enclosuresMap := make(map[int64]model.EnclosureList)
for rows.Next() {
var enclosure model.Enclosure
err := rows.Scan(
&enclosure.ID,
&enclosure.UserID,
&enclosure.EntryID,
&enclosure.URL,
&enclosure.Size,
&enclosure.MimeType,
&enclosure.MediaProgression,
)
if err != nil {
return nil, fmt.Errorf("store: unable to scan enclosure row: %w", err)
}
enclosuresMap[enclosure.EntryID] = append(enclosuresMap[enclosure.EntryID], &enclosure)
}
return enclosuresMap, nil
}
func (s *Storage) GetEnclosure(enclosureID int64) (*model.Enclosure, error) {
query := `
SELECT
+1 -11
View File
@@ -369,20 +369,10 @@ func (s *Storage) ArchiveEntries(status string, days, limit int) (int64, error)
// SetEntriesStatus update the status of the given list of entries.
func (s *Storage) SetEntriesStatus(userID int64, entryIDs []int64, status string) error {
query := `UPDATE entries SET status=$1, changed_at=now() WHERE user_id=$2 AND id=ANY($3)`
result, err := s.db.Exec(query, status, userID, pq.Array(entryIDs))
if err != nil {
if _, err := s.db.Exec(query, status, userID, pq.Array(entryIDs)); err != nil {
return fmt.Errorf(`store: unable to update entries statuses %v: %v`, entryIDs, err)
}
count, err := result.RowsAffected()
if err != nil {
return fmt.Errorf(`store: unable to update these entries %v: %v`, entryIDs, err)
}
if count == 0 {
return errors.New(`store: nothing has been updated`)
}
return nil
}
+28 -12
View File
@@ -277,7 +277,6 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
e.created_at,
e.changed_at,
e.tags,
(SELECT true FROM enclosures WHERE entry_id=e.id LIMIT 1) as has_enclosure,
f.title as feed_title,
f.feed_url,
f.site_url,
@@ -294,6 +293,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
f.hide_globally,
f.no_media_player,
fi.icon_id,
i.external_id AS icon_external_id,
u.timezone
FROM
entries e
@@ -303,6 +303,8 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
categories c ON c.id=f.category_id
LEFT JOIN
feed_icons fi ON fi.feed_id=f.id
LEFT JOIN
icons i ON i.id=fi.icon_id
LEFT JOIN
users u ON u.id=e.user_id
WHERE %s %s
@@ -319,10 +321,13 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
defer rows.Close()
entries := make(model.Entries, 0)
entryMap := make(map[int64]*model.Entry)
var entryIDs []int64
for rows.Next() {
var iconID sql.NullInt64
var externalIconID sql.NullString
var tz string
var hasEnclosure sql.NullBool
entry := model.NewEntry()
@@ -344,7 +349,6 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
&entry.CreatedAt,
&entry.ChangedAt,
pq.Array(&entry.Tags),
&hasEnclosure,
&entry.Feed.Title,
&entry.Feed.FeedURL,
&entry.Feed.SiteURL,
@@ -361,6 +365,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
&entry.Feed.HideGlobally,
&entry.Feed.NoMediaPlayer,
&iconID,
&externalIconID,
&tz,
)
@@ -368,20 +373,15 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
return nil, fmt.Errorf("store: unable to fetch entry row: %v", err)
}
if hasEnclosure.Valid && hasEnclosure.Bool && e.fetchEnclosures {
entry.Enclosures, err = e.store.GetEnclosures(entry.ID)
if err != nil {
return nil, fmt.Errorf("store: unable to fetch enclosures for entry #%d: %w", entry.ID, err)
}
}
if iconID.Valid {
if iconID.Valid && externalIconID.Valid && externalIconID.String != "" {
entry.Feed.Icon.FeedID = entry.FeedID
entry.Feed.Icon.IconID = iconID.Int64
entry.Feed.Icon.ExternalIconID = externalIconID.String
} else {
entry.Feed.Icon.IconID = 0
}
// Make sure that timestamp fields contains timezone information (API)
// Make sure that timestamp fields contain timezone information (API)
entry.Date = timezone.Convert(tz, entry.Date)
entry.CreatedAt = timezone.Convert(tz, entry.CreatedAt)
entry.ChangedAt = timezone.Convert(tz, entry.ChangedAt)
@@ -391,7 +391,23 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
entry.Feed.UserID = entry.UserID
entry.Feed.Icon.FeedID = entry.FeedID
entry.Feed.Category.UserID = entry.UserID
entries = append(entries, entry)
entryMap[entry.ID] = entry
entryIDs = append(entryIDs, entry.ID)
}
if e.fetchEnclosures && len(entryIDs) > 0 {
enclosures, err := e.store.GetEnclosuresForEntries(entryIDs)
if err != nil {
return nil, fmt.Errorf("store: unable to fetch enclosures: %w", err)
}
for entryID, entryEnclosures := range enclosures {
if entry, exists := entryMap[entryID]; exists {
entry.Enclosures = entryEnclosures
}
}
}
return entries, nil
+16 -5
View File
@@ -248,10 +248,11 @@ func (s *Storage) CreateFeed(feed *model.Feed) error {
apprise_service_urls,
webhook_url,
disable_http2,
description
description,
proxy_url
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27)
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)
RETURNING
id
`
@@ -284,6 +285,7 @@ func (s *Storage) CreateFeed(feed *model.Feed) error {
feed.WebhookURL,
feed.DisableHTTP2,
feed.Description,
feed.ProxyURL,
).Scan(&feed.ID)
if err != nil {
return fmt.Errorf(`store: unable to create feed %q: %v`, feed.FeedURL, err)
@@ -361,10 +363,12 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
description=$30,
ntfy_enabled=$31,
ntfy_priority=$32,
pushover_enabled=$33,
pushover_priority=$34
ntfy_topic=$33,
pushover_enabled=$34,
pushover_priority=$35,
proxy_url=$36
WHERE
id=$35 AND user_id=$36
id=$37 AND user_id=$38
`
_, err = s.db.Exec(query,
feed.FeedURL,
@@ -399,8 +403,10 @@ func (s *Storage) UpdateFeed(feed *model.Feed) (err error) {
feed.Description,
feed.NtfyEnabled,
feed.NtfyPriority,
feed.NtfyTopic,
feed.PushoverEnabled,
feed.PushoverPriority,
feed.ProxyURL,
feed.ID,
feed.UserID,
)
@@ -479,3 +485,8 @@ func (s *Storage) ResetFeedErrors() error {
_, err := s.db.Exec(`UPDATE feeds SET parsing_error_count=0, parsing_error_msg=''`)
return err
}
func (s *Storage) ResetNextCheckAt() error {
_, err := s.db.Exec(`UPDATE feeds SET next_check_at=now()`)
return err
}
+16 -6
View File
@@ -163,20 +163,25 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
c.title as category_title,
c.hide_globally as category_hidden,
fi.icon_id,
i.external_id,
u.timezone,
f.apprise_service_urls,
f.webhook_url,
f.disable_http2,
f.ntfy_enabled,
f.ntfy_priority,
f.ntfy_topic,
f.pushover_enabled,
f.pushover_priority
f.pushover_priority,
f.proxy_url
FROM
feeds f
LEFT JOIN
categories c ON c.id=f.category_id
LEFT JOIN
feed_icons fi ON fi.feed_id=f.id
LEFT JOIN
icons i ON i.id=fi.icon_id
LEFT JOIN
users u ON u.id=f.user_id
WHERE %s
@@ -200,6 +205,7 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
for rows.Next() {
var feed model.Feed
var iconID sql.NullInt64
var externalIconID sql.NullString
var tz string
feed.Category = &model.Category{}
@@ -236,24 +242,27 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
&feed.Category.Title,
&feed.Category.HideGlobally,
&iconID,
&externalIconID,
&tz,
&feed.AppriseServiceURLs,
&feed.WebhookURL,
&feed.DisableHTTP2,
&feed.NtfyEnabled,
&feed.NtfyPriority,
&feed.NtfyTopic,
&feed.PushoverEnabled,
&feed.PushoverPriority,
&feed.ProxyURL,
)
if err != nil {
return nil, fmt.Errorf(`store: unable to fetch feeds row: %w`, err)
}
if iconID.Valid {
feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.Int64}
if iconID.Valid && externalIconID.Valid {
feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.Int64, ExternalIconID: externalIconID.String}
} else {
feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: 0}
feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: 0, ExternalIconID: ""}
}
if readCounters != nil {
@@ -316,9 +325,10 @@ func (f *FeedQueryBuilder) fetchFeedCounter() (unreadCounters map[int64]int, rea
return nil, nil, fmt.Errorf(`store: unable to fetch feed counter row: %w`, err)
}
if status == model.EntryStatusRead {
switch status {
case model.EntryStatusRead:
readCounters[feedID] = count
} else if status == model.EntryStatusUnread {
case model.EntryStatusUnread:
unreadCounters[feedID] = count
}
}
+43 -8
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"strings"
"miniflux.app/v2/internal/crypto"
"miniflux.app/v2/internal/model"
)
@@ -22,8 +23,16 @@ func (s *Storage) HasFeedIcon(feedID int64) bool {
// IconByID returns an icon by the ID.
func (s *Storage) IconByID(iconID int64) (*model.Icon, error) {
var icon model.Icon
query := `SELECT id, hash, mime_type, content FROM icons WHERE id=$1`
err := s.db.QueryRow(query, iconID).Scan(&icon.ID, &icon.Hash, &icon.MimeType, &icon.Content)
query := `
SELECT
id,
hash,
mime_type,
content,
external_id
FROM icons
WHERE id=$1`
err := s.db.QueryRow(query, iconID).Scan(&icon.ID, &icon.Hash, &icon.MimeType, &icon.Content, &icon.ExternalID)
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
@@ -33,6 +42,29 @@ func (s *Storage) IconByID(iconID int64) (*model.Icon, error) {
return &icon, nil
}
// IconByExternalID returns an icon by the External Icon ID.
func (s *Storage) IconByExternalID(externalIconID string) (*model.Icon, error) {
var icon model.Icon
query := `
SELECT
id,
hash,
mime_type,
content,
external_id
FROM icons
WHERE external_id=$1
`
err := s.db.QueryRow(query, externalIconID).Scan(&icon.ID, &icon.Hash, &icon.MimeType, &icon.Content, &icon.ExternalID)
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("store: unable to fetch icon %s: %w", externalIconID, err)
}
return &icon, nil
}
// IconByFeedID returns a feed icon.
func (s *Storage) IconByFeedID(userID, feedID int64) (*model.Icon, error) {
query := `
@@ -40,7 +72,8 @@ func (s *Storage) IconByFeedID(userID, feedID int64) (*model.Icon, error) {
icons.id,
icons.hash,
icons.mime_type,
icons.content
icons.content,
icons.external_id
FROM icons
LEFT JOIN feed_icons ON feed_icons.icon_id=icons.id
LEFT JOIN feeds ON feeds.id=feed_icons.feed_id
@@ -49,7 +82,7 @@ func (s *Storage) IconByFeedID(userID, feedID int64) (*model.Icon, error) {
LIMIT 1
`
var icon model.Icon
err := s.db.QueryRow(query, userID, feedID).Scan(&icon.ID, &icon.Hash, &icon.MimeType, &icon.Content)
err := s.db.QueryRow(query, userID, feedID).Scan(&icon.ID, &icon.Hash, &icon.MimeType, &icon.Content, &icon.ExternalID)
if err != nil {
return nil, fmt.Errorf(`store: unable to fetch icon: %v`, err)
}
@@ -67,9 +100,9 @@ func (s *Storage) StoreFeedIcon(feedID int64, icon *model.Icon) error {
if err := tx.QueryRow(`SELECT id FROM icons WHERE hash=$1`, icon.Hash).Scan(&icon.ID); err == sql.ErrNoRows {
query := `
INSERT INTO icons
(hash, mime_type, content)
(hash, mime_type, content, external_id)
VALUES
($1, $2, $3)
($1, $2, $3, $4)
RETURNING
id
`
@@ -78,6 +111,7 @@ func (s *Storage) StoreFeedIcon(feedID int64, icon *model.Icon) error {
icon.Hash,
normalizeMimeType(icon.MimeType),
icon.Content,
crypto.GenerateRandomStringHex(20),
).Scan(&icon.ID)
if err != nil {
@@ -113,7 +147,8 @@ func (s *Storage) Icons(userID int64) (model.Icons, error) {
icons.id,
icons.hash,
icons.mime_type,
icons.content
icons.content,
icons.external_id
FROM icons
LEFT JOIN feed_icons ON feed_icons.icon_id=icons.id
LEFT JOIN feeds ON feeds.id=feed_icons.feed_id
@@ -129,7 +164,7 @@ func (s *Storage) Icons(userID int64) (model.Icons, error) {
var icons model.Icons
for rows.Next() {
var icon model.Icon
err := rows.Scan(&icon.ID, &icon.Hash, &icon.MimeType, &icon.Content)
err := rows.Scan(&icon.ID, &icon.Hash, &icon.MimeType, &icon.Content, &icon.ExternalID)
if err != nil {
return nil, fmt.Errorf(`store: unable to fetch icons row: %v`, err)
}
+12
View File
@@ -42,3 +42,15 @@ func (s *Storage) Ping() error {
func (s *Storage) DBStats() sql.DBStats {
return s.db.Stats()
}
// DBSize returns how much size the database is using in a pretty way.
func (s *Storage) DBSize() (string, error) {
var size string
err := s.db.QueryRow("SELECT pg_size_pretty(pg_database_size(current_database()))").Scan(&size)
if err != nil {
return "", err
}
return size, nil
}
@@ -10,7 +10,7 @@
<h2 id="feed-title-{{ .ID }}" class="item-title">
<a href="{{ route "feedEntries" "feedID" .ID }}">
{{ if and (.Icon) (gt .Icon.IconID 0) }}
<img src="{{ route "icon" "iconID" .Icon.IconID }}" width="16" height="16" loading="lazy" alt="">
<img src="{{ route "feedIcon" "externalIconID" .Icon.ExternalIconID }}" width="16" height="16" loading="lazy" alt="">
{{ end }}
{{ if .Disabled }} 🚫 {{ end }}
{{ .Title }}
@@ -22,10 +22,10 @@
title="{{ t "entry.status.title" }}"
data-toggle-status="true"
data-label-loading="{{ t "entry.state.saving" }}"
data-label-read="{{ t "entry.status.read" }}"
data-label-unread="{{ t "entry.status.unread" }}"
data-label-read="{{ t "entry.status.mark_as_read" }}"
data-label-unread="{{ t "entry.status.mark_as_unread" }}"
data-value="{{ if eq .entry.Status "read" }}read{{ else }}unread{{ end }}"
>{{ if eq .entry.Status "read" }}{{ icon "unread" }}{{ else }}{{ icon "read" }}{{ end }}<span class="icon-label">{{ if eq .entry.Status "read" }}{{ t "entry.status.unread" }}{{ else }}{{ t "entry.status.read" }}{{ end }}</span></button>
>{{ if eq .entry.Status "read" }}{{ icon "unread" }}{{ else }}{{ icon "read" }}{{ end }}<span class="icon-label">{{ if eq .entry.Status "read" }}{{ t "entry.status.mark_as_unread" }}{{ else }}{{ t "entry.status.mark_as_read" }}{{ end }}</span></button>
</li>
<li class="item-meta-icons-star">
<button
@@ -18,6 +18,7 @@
<li><strong>{{ t "page.about.build_date" }}</strong> {{ .build_date }}</li>
{{ if .user.IsAdmin }}<li><strong>{{ t "page.about.postgres_version" }}</strong> {{ .postgres_version }}</li>{{ end }}
<li><strong>{{t "page.about.go_version" }}</strong> {{ .go_version }}</li>
{{ if .user.IsAdmin }}<li><strong>{{t "page.about.db_usage" }}</strong> {{ .db_usage }}</li>{{ end }}
</ul>
</div>
@@ -39,6 +39,9 @@
<label><input type="checkbox" name="fetch_via_proxy" value="1" {{ if .form.FetchViaProxy }}checked{{ end }}> {{ t "form.feed.label.fetch_via_proxy" }}</label>
{{ end }}
<label for="form-proxy-url">{{ t "form.feed.label.proxy_url" }}</label>
<input type="url" name="proxy_url" id="form-proxy-url" value="{{ .form.ProxyURL }}" spellcheck="false">
<label for="form-user-agent">{{ t "form.feed.label.user_agent" }}</label>
<input type="text" name="user_agent" id="form-user-agent" placeholder="{{ .defaultUserAgent }}" value="{{ .form.UserAgent }}" spellcheck="false" autocomplete="off">

Some files were not shown because too many files have changed in this diff Show More