Compare commits

..

25 Commits

Author SHA1 Message Date
jeffvli be0ebac362 Update to v0.12.1 2024-11-20 10:50:04 -08:00
Mitja Ševerkar 8eb8290fc4 Fix URL encoding on Subsonic (#850)
* Revert "Encode credential for subsonic stream/coverart (#841)"

This reverts commit 8ec4551b46.

* Properly URL encode credentials on Subsonic

Previous commit (8ec4551b46) has been reverted, as it has encoded even equal signs (=), and and signs (&), which should not have been encoded. Nextcloud Music has subsequently failed to receive separate username and password and has therefore failed whilst authenticating the user.

Example of URL beforehand:
https://cloud.example.com/index.php/apps/music/subsonic/rest/stream.view?id=track-4936&v=1.13.0&c=feishin_&u%3Dtest-test%40example.com%26p%3Dpassword

Example of URL now:
https://cloud.example.com/index.php/apps/music/subsonic/rest/stream.view?id=track-4936&v=1.13.0&c=feishin_&u=test-test%40example.com&p=password
2024-11-19 19:00:53 -08:00
jeffvli fac1d3fb62 Update to v0.12.0 2024-11-18 20:43:41 -08:00
Hosted Weblate 93fbe1f49a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (657 of 657 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-11-19 04:37:48 +00:00
Hosted Weblate 59f17a4faa Translated using Weblate (Korean)
Currently translated at 35.0% (230 of 657 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Korean)

Currently translated at 26.5% (174 of 655 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: tgp0625 <tgp0625@naver.com>
Co-authored-by: ᄒᄋ <prohack1109@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ko/
Translation: feishin/Translation
2024-11-19 04:37:47 +00:00
Hosted Weblate d9e41720c8 Translated using Weblate (Spanish)
Currently translated at 100.0% (657 of 657 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2024-11-19 04:37:46 +00:00
Hosted Weblate 8452780602 Translated using Weblate (Czech)
Currently translated at 100.0% (657 of 657 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2024-11-19 04:37:46 +00:00
jeffvli 96b5b660fb Add maintenance notice 2024-11-18 20:37:33 -08:00
Martin 610138c05c README.md: Colorize YAML block, fix macOS capitalization and remove deprecated compose version (#847) 2024-11-18 20:22:13 -08:00
jeffvli 6a619240fa Handle potential undefined value on CardRows (#834) 2024-11-18 20:17:33 -08:00
jeffvli b65c972da1 Handle negative values on gain calculation (#834) 2024-11-18 20:16:20 -08:00
jeffvli 8ec4551b46 Encode credential for subsonic stream/coverart (#841) 2024-11-13 17:54:54 -08:00
Jack Merrill 21f4a78dd7 feat: Discord Rich Presence album art via Last.fm (#341) (#817)
* feat: Discord Rich Presence album art via Last.fm

* fix: securely fetch album art
2024-10-31 12:09:17 -07:00
sel10ut 61d7e7c390 fix(jellyfin): return "Appears On" section to artist page (#812)
Exclude 'AlbumArtistIds' when querying "Appears On" items, which,
if put together with 'ContributingArtistIds', returns an empty list.
2024-10-31 11:33:10 -07:00
astrid 993841ddbf fix devEngines (#801) 2024-10-31 11:31:58 -07:00
jeffvli 98b8409592 Update description to include subsonic servers 2024-10-15 03:22:21 -07:00
jeffvli d3480a86c3 Fix release date parsing to use UTC (#794) 2024-10-15 03:15:59 -07:00
jeffvli 3a63ee4b95 Include all playlist types in Jellyfin playlist fetch 2024-10-15 03:04:38 -07:00
jeffvli 876376d65f Update to v0.11.1 2024-10-14 20:26:21 -07:00
Hosted Weblate 215abf615d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (655 of 655 strings)

Co-authored-by: ENDzZ <godzmichael@outlook.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hans/
Translation: feishin/Translation
2024-10-15 04:37:52 +02:00
Hosted Weblate afad2843c6 Translated using Weblate (Spanish)
Currently translated at 100.0% (655 of 655 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translation: feishin/Translation
2024-10-15 04:37:52 +02:00
Hosted Weblate 958ab1f31f Translated using Weblate (Czech)
Currently translated at 100.0% (655 of 655 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translation: feishin/Translation
2024-10-15 04:37:51 +02:00
Hosted Weblate 0ca325aac2 Translated using Weblate (Portuguese (Brazil))
Currently translated at 31.1% (204 of 655 strings)

Co-authored-by: Rafael Vieira <rafaelvieiras@pm.me>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pt_BR/
Translation: feishin/Translation
2024-10-15 04:37:50 +02:00
jeffvli 12b66e5fa0 Convert subsonic coverart property to string (#795) 2024-10-14 19:36:51 -07:00
jeffvli 7e78478fbe Fix combined title cell controls blocking links 2024-10-14 00:38:28 -07:00
25 changed files with 431 additions and 64 deletions
+2 -4
View File
@@ -1,5 +1,5 @@
name: Feature request
description: Request a feature to be added to Feishin 🎉
name: Feature request - NOT ACCEPTING NEW FEATURE REQUESTS
description: Feature requests are currently closed. The application is actively being rewritten https://github.com/audioling/audioling.
labels: ['enhancement']
body:
- type: textarea
@@ -18,5 +18,3 @@ body:
options:
- label: 'Yes'
required: false
validations:
required: false
+37 -20
View File
@@ -27,6 +27,16 @@
</a>
</p>
---
## MAINTENANCE NOTICE
Feishin is currently undergoing a major rewrite. New feature requests will not be accepted. The rewrite is being actively developed at the [audioling](https://github.com/audioling/audioling) repository.
Follow the repository or join the discord/matrix server for updates.
---
Rewrite of [Sonixd](https://github.com/jeffvli/sonixd).
## Features
@@ -49,7 +59,7 @@ Rewrite of [Sonixd](https://github.com/jeffvli/sonixd).
Download the [latest desktop client](https://github.com/jeffvli/feishin/releases). The desktop client is the recommended way to use Feishin. It supports both the MPV and web player backends, as well as includes built-in fetching for lyrics.
#### MacOS Notes
#### macOS Notes
If you're using a device running macOS 12 (Monterey) or higher, [check here](https://github.com/jeffvli/feishin/issues/104#issuecomment-1553914730) for instructions on how to remove the app from quarantine.
@@ -74,24 +84,23 @@ docker run --name feishin -p 9180:9180 feishin
To install via Docker Compose use the following snippit. This also works on Portainer.
```
version: '3'
```yaml
services:
feishin:
container_name: feishin
image: 'ghcr.io/jeffvli/feishin:latest'
environment:
- SERVER_NAME=jellyfin # pre defined server name
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
- SERVER_TYPE=jellyfin # navidrome also works
- SERVER_URL= # http://address:port
- PUID=1000
- PGID=1000
- UMASK=002
- TZ=America/Los_Angeles
ports:
- 9180:9180
restart: unless-stopped
feishin:
container_name: feishin
image: 'ghcr.io/jeffvli/feishin:latest'
environment:
- SERVER_NAME=jellyfin # pre defined server name
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
- SERVER_TYPE=jellyfin # navidrome also works
- SERVER_URL= # http://address:port
- PUID=1000
- PGID=1000
- UMASK=002
- TZ=America/Los_Angeles
ports:
- 9180:9180
restart: unless-stopped
```
### Configuration
@@ -119,8 +128,16 @@ Feishin supports any music server that implements a [Navidrome](https://www.navi
- [Navidrome](https://github.com/navidrome/navidrome)
- [Jellyfin](https://github.com/jellyfin/jellyfin)
- [Funkwhale](https://funkwhale.audio/) - TBD
- Subsonic-compatible servers - TBD
- Subsonic-compatible servers
- [Airsonic-Advanced](https://github.com/airsonic-advanced/airsonic-advanced)
- [Ampache](https://ampache.org)
- [Astiga](https://asti.ga/)
- [Funkwhale](https://www.funkwhale.audio/)
- [Gonic](https://github.com/sentriz/gonic)
- [LMS](https://github.com/epoupon/lms)
- [Nextcloud Music](https://apps.nextcloud.com/apps/music)
- [Supysonic](https://github.com/spl0k/supysonic)
- More (?)
### I have the issue "The SUID sandbox helper binary was found, but is not configured correctly" on Linux
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "feishin",
"version": "0.11.0",
"version": "0.12.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "feishin",
"version": "0.11.0",
"version": "0.12.1",
"hasInstallScript": true,
"license": "GPL-3.0",
"dependencies": {
+11 -3
View File
@@ -2,7 +2,7 @@
"name": "feishin",
"productName": "Feishin",
"description": "Feishin music server",
"version": "0.11.0",
"version": "0.12.1",
"scripts": {
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"",
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
@@ -360,8 +360,16 @@
"styled-components": "^6"
},
"devEngines": {
"node": ">=18.x",
"npm": ">=7.x"
"runtime": {
"name": "node",
"version": ">=18.x",
"onFail": "error"
},
"packageManager": {
"name": "npm",
"version": ">=7.x",
"onFail": "error"
}
},
"browserslist": [],
"electronmon": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "feishin",
"version": "0.11.0",
"version": "0.12.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "feishin",
"version": "0.11.0",
"version": "0.12.1",
"hasInstallScript": true,
"license": "GPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "feishin",
"version": "0.11.0",
"version": "0.12.1",
"description": "",
"main": "./dist/main/main.js",
"author": {
+7 -3
View File
@@ -255,7 +255,9 @@
"translationApiKey": "klíč api překladů",
"translationApiKey_description": "klíč api pro překlady (podporuje pouze koncový bod globální služby)",
"translationTargetLanguage": "cílový jazyk překladu",
"translationTargetLanguage_description": "cílový jazyk pro překlad"
"translationTargetLanguage_description": "cílový jazyk pro překlad",
"lastfmApiKey": "klíč API {{lastfm}}",
"lastfmApiKey_description": "klíč API pro {{lastfm}}. vyžadováno pro obaly alb"
},
"action": {
"editPlaylist": "upravit $t(entity.playlist_one)",
@@ -278,7 +280,8 @@
"openIn": {
"lastfm": "Otevřít v Last.fm",
"musicbrainz": "Otevřít v MusicBrainz"
}
},
"moveToNext": "přesunout na další"
},
"common": {
"backward": "zpátky",
@@ -588,7 +591,8 @@
"shareItem": "sdílet položku",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"download": "stáhnout",
"playShuffled": "$t(player.shuffle)"
"playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)"
},
"home": {
"mostPlayed": "nejpřehrávanější",
+2
View File
@@ -582,6 +582,8 @@
"imageAspectRatio_description": "if enabled, cover art will be shown using their native aspect ratio. for art that is not 1:1, the remaining space will be empty",
"language": "language",
"language_description": "sets the language for the application ($t(common.restartRequired))",
"lastfmApiKey": "{{lastfm}} API key",
"lastfmApiKey_description": "the API key for {{lastfm}}. required for cover art",
"lyricFetch": "fetch lyrics from the internet",
"lyricFetch_description": "fetch lyrics from various internet sources",
"lyricFetchProvider": "providers to fetch lyrics from",
+7 -3
View File
@@ -255,7 +255,9 @@
"translationApiKey": "clave api de traducción",
"translationApiKey_description": "Clave API para la traducción (solo para el punto final del servicio global)",
"translationTargetLanguage": "idioma final de la traducción",
"translationTargetLanguage_description": "lengua de destino de la traducción"
"translationTargetLanguage_description": "lengua de destino de la traducción",
"lastfmApiKey_description": "la clave API para {{lastfm}}. Requerida para la portada",
"lastfmApiKey": "Clave API para {{lastfm}}"
},
"action": {
"editPlaylist": "editar $t(entity.playlist_one)",
@@ -278,7 +280,8 @@
"openIn": {
"lastfm": "Abrir en Last.fm",
"musicbrainz": "Abrir en MusicBrainz"
}
},
"moveToNext": "pasar al siguiente"
},
"common": {
"backward": "hacia atrás",
@@ -490,7 +493,8 @@
"showDetails": "Obtener información",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"download": "descargar",
"playShuffled": "$t(player.shuffle)"
"playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)"
},
"home": {
"mostPlayed": "más reproducidos",
+273 -1
View File
@@ -9,6 +9,278 @@
"editPlaylist": "$t(entity.playlist_one) 편집",
"goToPage": "페이지 이동",
"moveToBottom": "맨 아래로 이동",
"moveToTop": "맨 위로 이동"
"moveToTop": "맨 위로 이동",
"moveToNext": "다음으로 이동",
"removeFromQueue": "대기열에서 제거",
"refresh": "$t(common.refresh)",
"removeFromFavorites": "$t(entity.favorite_other)에서 제거",
"removeFromPlaylist": "$t(entity.playlist_one)에서 제거",
"openIn": {
"musicbrainz": "MusicBrainz에서 보기",
"lastfm": "Last.fm에서 보기"
},
"viewPlaylists": "$t(entity.playlist_other) 보기",
"setRating": "평점 지정"
},
"common": {
"translation": "번역",
"resetToDefault": "기본 설정으로 되돌리기",
"right": "오른쪽",
"save": "저장",
"increase": "증가",
"version": "버전",
"year": "년",
"reset": "초기화",
"random": "랜덤",
"close": "닫기",
"codec": "코덱",
"create": "만들기",
"disc": "디스크",
"gap": "갭",
"left": "왼쪽",
"add": "추가",
"backward": "뒤로",
"saveAs": "(으)로 저장하기",
"search": "검색",
"setting": "설정",
"share": "공유",
"size": "크기",
"sortOrder": "순서",
"title": "곡명",
"trackNumber": "트랙번호",
"trackGain": "트랙 게인",
"trackPeak": "트랙 피크",
"unknown": "알 수 없음",
"cancel": "취소",
"clear": "지우기",
"collapse": "접기",
"comingSoon": "조만간…",
"configure": "설정",
"confirm": "확인",
"currentSong": "현재 $t(entity.track_one)",
"decrease": "감소",
"delete": "삭제",
"descending": "내림차순",
"description": "설명",
"disable": "비활성",
"edit": "편집",
"enable": "활성",
"expand": "확장",
"favorite": "즐겨찾기",
"forceRestartRequired": "변경 사항을 적용하려면 재실행 하세요... 알림을 닫으면 재실행합니다",
"forward": "앞으로",
"limit": "제한",
"manage": "관리하다",
"maximize": "최대화",
"menu": "메뉴",
"minimize": "최소화",
"modified": "수정된",
"name": "이름",
"path": "경로",
"playerMustBePaused": "플레이어가 일시정지 되어야 합니다",
"preview": "미리보기",
"previousSong": "이전곡 $t(entity.track_one)",
"quit": "종료",
"refresh": "새로고침",
"reload": "리로드",
"restartRequired": "반드시 재실행되어야 합니다",
"saveAndReplace": "저장하고 변경하기",
"yes": "네",
"ascending": "오름차순",
"areYouSure": "확실한가요?",
"bitrate": "비트 전송률",
"bpm": "bpm",
"biography": "바이오그래피",
"center": "중앙",
"channel_other": "채널",
"filter_other": "필터",
"mbid": "MusicBrainz ID",
"dismiss": "닫기",
"duration": "길이",
"home": "홈",
"no": "아니오",
"none": "없음",
"rating": "평점"
},
"entity": {
"albumWithCount_other": "{{count}} 앨범",
"artist_other": "아티스트",
"artistWithCount_other": "{{count}} 아티스트",
"favorite_other": "즐겨찾기",
"folder_other": "폴더",
"genre_other": "장르",
"genreWithCount_other": "{{count}} 장르",
"playlist_other": "플레이리스트",
"album_other": "앨범",
"albumArtist_other": "앨범 아티스트",
"albumArtistCount_other": "{{count}} 앨범 아티스트",
"folderWithCount_other": "{{count}} 폴더",
"trackWithCount_other": "{{count}} 트랙",
"song_other": "곡",
"play_other": "{{count}} 재생",
"playlistWithCount_other": "{{count}} 재생목록",
"smartPlaylist": "스마트 $t(entity.playlist_one)",
"track_other": "트랙"
},
"error": {
"systemFontError": "시스템 폰트를 가져오는데 실패하였습니다",
"loginRateError": "너무 많은 로그인 시도하였습니다 잠시 후 다시 시도해 주세요",
"mpvRequired": "MPV 필요",
"openError": "파일을 열 수 없습니다",
"remoteDisableError": "원격 서버를 $t(common.disable) 하는데 실패하였습니다",
"playbackError": "미디어를 재생하는 도중에 에러가 발생하였습니다",
"remoteEnableError": "원격 서버를 $t(common.enable) 하는데 실패하였습니다",
"serverNotSelectedError": "선택된 서버가 없습니다",
"serverRequired": "서버가 필요합니다",
"sessionExpiredError": "세션이 만료되었습니다",
"networkError": "네트워크 에러가 발생하였습니다",
"remotePortError": "원격 서버의 포트 설정하는데 실패하였습니다",
"remotePortWarning": "새로 설정한 포트를 적용하기 위해 서버를 재실행 해 주세요",
"audioDeviceFetchError": "오디오 장치를 불러올 수 없습니다",
"authenticationFailed": "인증 실패",
"badAlbum": "이 곡은 앨범의 일부가 아니기 때문에 표시되는 것입니다. 음악 폴더의 최상위에 곡이 있는 경우 이런 문제가 발생할 가능성이 높습니다. Jellyfin은 폴더 내 그룹만 추적합니다.",
"credentialsRequired": "인증서가 필요함",
"endpointNotImplementedError": "엔드포인트 {{endpoint}} 는 {{serverType}} 에 대해 구현되지 않았습니다",
"genericError": "에러가 발생했습니다",
"invalidServer": "잘못된 서버",
"localFontAccessDenied": "로컬 글꼴에 접근 거부되었습니다"
},
"filter": {
"title": "곡명",
"isRecentlyPlayed": "최근에 재생한",
"name": "이름",
"path": "경로",
"playCount": "재생 횟수",
"random": "무작위",
"recentlyAdded": "최근에 추가된",
"releaseDate": "발매일",
"recentlyPlayed": "최근에 재생된",
"recentlyUpdated": "최근에 업데이트된",
"search": "검색",
"dateAdded": "추가된 날짜",
"lastPlayed": "마지막으로 재생한",
"mostPlayed": "가장 많이 재생한",
"album": "$t(entity.album_one)",
"albumArtist": "$t(entity.albumArtist_one)",
"artist": "$t(entity.artist_one)",
"communityRating": "커뮤니티 평점",
"criticRating": "비평가 평점",
"disc": "디스크",
"bitrate": "비트 전송률",
"biography": "바이오그래피",
"channels": "$t(common.channel_other)",
"duration": "길이",
"bpm": "bpm"
},
"form": {
"addServer": {
"title": "서버 추가하기",
"success": "서버 추가하였습니다",
"input_name": "서버 이름",
"input_password": "비밀번호",
"input_savePassword": "비밀번호 저장하기",
"input_url": "url",
"error_savePassword": "비밀번호를 저장하는 도중 오류가 발생했습니다",
"ignoreCors": "CORS 무시 ($t(common.restartRequired))",
"ignoreSsl": "SSL 무시 ($t(common.restartRequired))",
"input_legacyAuthentication": "레거시 인증 사용",
"input_username": "유저 이름"
},
"addToPlaylist": {
"input_skipDuplicates": "중복 건너뛰기",
"title": "$t(entity.playlist_one) 에 추가",
"input_playlists": "$t(entity.playlist_other)"
},
"lyricSearch": {
"title": "가사 검색",
"input_name": "$t(common.name)",
"input_artist": "$t(entity.artist_one)"
},
"queryEditor": {
"input_optionMatchAll": "모두 일치",
"input_optionMatchAny": "무엇이든 일치"
},
"editPlaylist": {
"title": "$t(entity.playlist_one) 편집",
"publicJellyfinNote": "Jellyfin은 재생목록 공개 여부를 노출하지 않습니다. 만약 공개되길 원한다면 다음을 선택하세요",
"success": "$t(entity.playlist_one) 업데이트 되었습니다"
},
"shareItem": {
"allowDownloading": "다운로드 허용",
"description": "설명",
"success": "클립보드에 공유 링크를 복사했습니다 (또는 열어보려면 클릭하세요)",
"expireInvalid": "만료 날짜는 미래 날짜여야만 합니다",
"createFailed": "공유 링크를 생성하는데 실패하였습니다 (혹시 공유하기 설정되어 있나요?)",
"setExpiration": "만료 기간 설정하기"
},
"updateServer": {
"title": "서버 업데이트",
"success": "서버 업데이트 되었습니다"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"input_name": "$t(common.name)",
"success": "$t(entity.playlist_one)를 생성했습니다",
"input_owner": "$t(common.owner)",
"input_public": "공개",
"title": "$t(entity.playlist_one) 생성"
},
"deletePlaylist": {
"input_confirm": "확인을 위해 $t(entity.playlist_one)의 이름을 적어주세요",
"success": "$t(entity.playlist_one)가 삭제되었습니다",
"title": "$t(entity.playlist_one) 삭제"
}
},
"page": {
"appMenu": {
"goBack": "뒤로",
"selectServer": "서버를 선택하세요",
"goForward": "앞으로",
"manageServers": "서버 설정하기",
"openBrowserDevtools": "브라우저 개발자 도구 열기",
"version": "버전 {{version}}"
},
"manageServers": {
"title": "서버 설정하기",
"serverDetails": "서버 세부설정",
"editServerDetailsTooltip": "서버 세부설정 편집하기",
"url": "URL",
"username": "username",
"removeServer": "서버 제거하기"
},
"fullscreenPlayer": {
"config": {
"opacity": "투명도",
"lyricAlignment": "가사 정렬",
"useImageAspectRatio": "이미지 종횡비 사용",
"synchronized": "동기화",
"unsynchronized": "비동기화"
},
"lyrics": "가사"
},
"contextMenu": {
"download": "다운로드",
"numberSelected": "{{count}}개 선택됨"
},
"albumArtistDetail": {
"about": "{{artist}}에 대해",
"viewDiscography": "디스코그래피 보기",
"appearsOn": "참여 앨범",
"recentReleases": "최근 앨범",
"relatedArtists": "연관 $t(entity.artist_other)"
}
},
"table": {
"config": {
"label": {
"playCount": "재생 횟수",
"dateAdded": "추가된 날짜"
},
"view": {
"card": "카드",
"poster": "포스터",
"table": "표"
}
}
}
}
+5 -2
View File
@@ -86,7 +86,8 @@
"codec": "codec",
"preview": "pré-visualizar",
"share": "compartilhar",
"close": "fechar"
"close": "fechar",
"translation": "tradução"
},
"action": {
"goToPage": "vá para página",
@@ -108,7 +109,9 @@
"openIn": {
"lastfm": "Abrir em Last.fm",
"musicbrainz": "Abrir em MusicBrainz"
}
},
"toggleSmartPlaylistEditor": "alternar editor $t(entity.smartPlaylist)",
"moveToNext": "mover para o próximo"
},
"form": {
"deletePlaylist": {
+7 -3
View File
@@ -20,7 +20,8 @@
"openIn": {
"lastfm": "在 Last.fm 中打开",
"musicbrainz": "在 MusicBrainz 中打开"
}
},
"moveToNext": "移至下一首"
},
"common": {
"increase": "增高",
@@ -386,7 +387,9 @@
"translationApiKey": "翻译api密钥",
"translationApiKey_description": "翻译api密钥(仅支持全球服务节点)",
"translationTargetLanguage": "目标翻译语言",
"translationTargetLanguage_description": "目标翻译语言"
"translationTargetLanguage_description": "目标翻译语言",
"lastfmApiKey": "{{lastfm}} API 密钥",
"lastfmApiKey_description": "{{lastfm}} 的 API 密钥。封面艺术图所需"
},
"error": {
"remotePortWarning": "重启服务器使新端口生效",
@@ -554,7 +557,8 @@
"shareItem": "分享项目",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"download": "下载",
"playShuffled": "$t(player.shuffle)"
"playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)"
},
"trackList": {
"title": "$t(entity.track_other)",
@@ -293,10 +293,14 @@ export const JellyfinController: ControllerEndpoint = {
userId: apiClientProps.server?.userId,
},
query: {
AlbumArtistIds: query.artistIds
? formatCommaDelimitedString(query.artistIds)
: undefined,
ContributingArtistIds: query.compilation ? query.artistIds?.[0] : undefined,
...(!query.compilation &&
query.artistIds && {
AlbumArtistIds: formatCommaDelimitedString(query.artistIds),
}),
...(query.compilation &&
query.artistIds && {
ContributingArtistIds: query.artistIds[0],
}),
GenreIds: query.genres ? query.genres.join(',') : undefined,
IncludeItemTypes: 'MusicAlbum',
IsFavorite: query.favorite,
@@ -450,7 +454,6 @@ export const JellyfinController: ControllerEndpoint = {
Fields: 'ChildCount, Genres, DateCreated, ParentId, Overview',
IncludeItemTypes: 'Playlist',
Limit: query.limit,
MediaTypes: 'Audio',
Recursive: true,
SearchTerm: query.searchTerm,
SortBy: playlistListSortMap.jellyfin[query.sortBy],
@@ -121,7 +121,7 @@ const normalizeSong = (
playlistItemId,
releaseDate: (item.releaseDate
? new Date(item.releaseDate)
: new Date(item.year, 0, 1)
: new Date(Date.UTC(item.year, 0, 1))
).toISOString(),
releaseYear: String(item.year),
serverId: server?.id || 'unknown',
@@ -65,7 +65,7 @@ export const SubsonicController: ControllerEndpoint = {
const cleanServerUrl = `${url.replace(/\/$/, '')}/rest`;
if (body.legacy) {
credential = `u=${body.username}&p=${body.password}`;
credential = `u=${encodeURIComponent(body.username)}&p=${encodeURIComponent(body.password)}`;
credentialParams = {
p: body.password,
u: body.username,
@@ -73,7 +73,7 @@ export const SubsonicController: ControllerEndpoint = {
} else {
const salt = randomString(12);
const hash = md5(body.password + salt);
credential = `u=${body.username}&s=${salt}&t=${hash}`;
credential = `u=${encodeURIComponent(body.username)}&s=${encodeURIComponent(salt)}&t=${encodeURIComponent(hash)}`;
credentialParams = {
s: salt,
t: hash,
@@ -43,7 +43,7 @@ const normalizeSong = (
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
coverArtId: item.coverArt?.toString(),
credential: server?.credential,
size: size || 300,
}) || null;
@@ -135,7 +135,7 @@ const normalizeAlbumArtist = (
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
coverArtId: item.coverArt?.toString(),
credential: server?.credential,
size: imageSize || 100,
}) || null;
@@ -170,7 +170,7 @@ const normalizeAlbum = (
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
coverArtId: item.coverArt?.toString(),
credential: server?.credential,
size: imageSize || 300,
}) || null;
@@ -207,7 +207,7 @@ const normalizeAlbum = (
name: item.name,
originalDate: null,
playCount: null,
releaseDate: item.year ? new Date(item.year, 0, 1).toISOString() : null,
releaseDate: item.year ? new Date(Date.UTC(item.year, 0, 1)).toISOString() : null,
releaseYear: item.year ? Number(item.year) : null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
@@ -238,7 +238,7 @@ const normalizePlaylist = (
imagePlaceholderUrl: null,
imageUrl: getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
coverArtId: item.coverArt?.toString(),
credential: server?.credential,
size: 300,
}),
@@ -341,7 +341,7 @@ export const AudioPlayer = forwardRef(
// Set the current replaygain
if (current) {
const newVolume = calculateReplayGain(current) * volume;
webAudio.gain.gain.setValueAtTime(newVolume, 0);
webAudio.gain.gain.setValueAtTime(Math.max(0, newVolume), 0);
}
// Set the next track replaygain right before the end of this track
@@ -349,7 +349,10 @@ export const AudioPlayer = forwardRef(
const next = sources[3 - currentPlayer];
if (next && current) {
const newVolume = calculateReplayGain(next) * volume;
webAudio.gain.gain.setValueAtTime(newVolume, (current.duration - 1) / 1000);
webAudio.gain.gain.setValueAtTime(
Math.max(0, newVolume),
Math.max(0, (current.duration - 1) / 1000),
);
}
}, [
calculateReplayGain,
+1 -1
View File
@@ -202,7 +202,7 @@ export const AlbumCard = ({
<ImageSection />
</Skeleton>
<DetailSection style={{ width: '100%' }}>
{cardRows.map((_row: CardRow<Album>, index: number) => (
{(cardRows || []).map((_row: CardRow<Album>, index: number) => (
<Skeleton
visible
height={15}
+1 -1
View File
@@ -191,7 +191,7 @@ export const PosterCard = ({
</Skeleton>
<DetailContainer>
<Stack spacing="sm">
{controls.cardRows.map((row, index) => (
{(controls?.cardRows || []).map((row, index) => (
<Skeleton
key={`${index}-${row.arrayProperty}`}
visible
@@ -234,7 +234,7 @@ export const DefaultCard = ({
</ImageContainer>
<DetailContainer>
<Stack spacing="sm">
{controls.cardRows.map((row, index) => (
{(controls?.cardRows || []).map((row, index) => (
<Skeleton
key={`${index}-${columnIndex}-${row.arrayProperty}`}
visible
@@ -219,7 +219,7 @@ export const PosterCard = ({
</Skeleton>
<DetailContainer>
<Stack spacing="sm">
{controls.cardRows.map((row, index) => (
{(controls?.cardRows || []).map((row, index) => (
<Skeleton
key={`${index}-${columnIndex}-${row.arrayProperty}`}
visible
@@ -38,6 +38,7 @@ const CellContainer = styled(motion.div)<{ height: number }>`
`;
const ImageWrapper = styled.div`
position: relative;
display: flex;
grid-area: image;
align-items: center;
@@ -5,6 +5,7 @@ import {
useCurrentSong,
useCurrentStatus,
useDiscordSetttings,
useGeneralSettings,
usePlayerStore,
} from '/@/renderer/store';
import { SetActivity } from '@xhayper/discord-rpc';
@@ -16,6 +17,7 @@ const discordRpc = isElectron() ? window.electron.discordRpc : null;
export const useDiscordRpc = () => {
const intervalRef = useRef(0);
const discordSettings = useDiscordSetttings();
const generalSettings = useGeneralSettings();
const currentSong = useCurrentSong();
const currentStatus = useCurrentStatus();
@@ -67,6 +69,19 @@ export const useDiscordRpc = () => {
activity.largeImageKey = song?.imageUrl;
}
if (generalSettings.lastfmApiKey && song?.album && song?.artists.length) {
console.log('Fetching album info for', song.album, song.artists[0].name);
const albumInfo = await fetch(
`https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=${generalSettings.lastfmApiKey}&artist=${encodeURIComponent(song.artistName)}&album=${encodeURIComponent(song.album)}&format=json`,
);
const albumInfoJson = await albumInfo.json();
if (albumInfoJson.album?.image?.[3]['#text']) {
activity.largeImageKey = albumInfoJson.album.image[3]['#text'];
}
}
// Fall back to default icon if not set
if (!activity.largeImageKey) {
activity.largeImageKey = 'icon';
@@ -79,6 +94,7 @@ export const useDiscordRpc = () => {
discordSettings.enableIdle,
discordSettings.showAsListening,
discordSettings.showServerImage,
generalSettings.lastfmApiKey,
]);
useEffect(() => {
@@ -4,12 +4,17 @@ import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import { useDiscordSetttings, useSettingsStoreActions } from '/@/renderer/store';
import {
useDiscordSetttings,
useSettingsStoreActions,
useGeneralSettings,
} from '/@/renderer/store';
import { useTranslation } from 'react-i18next';
export const DiscordSettings = () => {
const { t } = useTranslation();
const settings = useDiscordSetttings();
const generalSettings = useGeneralSettings();
const { setSettings } = useSettingsStoreActions();
const discordOptions: SettingOption[] = [
@@ -142,6 +147,31 @@ export const DiscordSettings = () => {
postProcess: 'sentenceCase',
}),
},
{
control: (
<TextInput
defaultValue={generalSettings.lastfmApiKey}
onBlur={(e) => {
setSettings({
general: {
...generalSettings,
lastfmApiKey: e.currentTarget.value,
},
});
}}
/>
),
description: t('setting.lastfmApiKey', {
context: 'description',
lastfm: 'Last.fm',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.lastfmApiKey', {
lastfm: 'Last.fm',
postProcess: 'sentenceCase',
}),
},
];
return <SettingsSection options={discordOptions} />;
+2
View File
@@ -232,6 +232,7 @@ export interface SettingsState {
homeFeature: boolean;
homeItems: SortableItem<HomeItem>[];
language: string;
lastfmApiKey: string;
nativeAspectRatio: boolean;
passwordStore?: string;
playButtonBehavior: Play;
@@ -377,6 +378,7 @@ const initialState: SettingsState = {
homeFeature: true,
homeItems,
language: 'en',
lastfmApiKey: '',
nativeAspectRatio: false,
passwordStore: undefined,
playButtonBehavior: Play.NOW,