Compare commits

..

71 Commits

Author SHA1 Message Date
jeffvli 9ae4be9336 add missing publish to win beta 2025-10-12 01:05:54 -07:00
jeffvli 1686e7ad0b add separate beta publish scripts 2025-10-12 00:54:07 -07:00
jeffvli bf6047da17 add separate electron builder config for beta channel 2025-10-12 00:53:05 -07:00
jeffvli c3b4a9edf8 attempt fix on beta channel release files 2025-10-11 20:25:33 -07:00
jeffvli 0340cc8a85 update to v0.21.0 2025-10-11 20:12:17 -07:00
jeffvli 3f7a402ce8 add commit notes to beta deploy 2025-10-11 20:05:26 -07:00
jeffvli 20c585aa1c remove unneeded tag deletion 2025-10-11 19:43:08 -07:00
jeffvli 0248997a75 delete old tags in addition to release 2025-10-11 19:41:19 -07:00
jeffvli aaaeea1fa5 split workflow into separate jobs, fix release rename step 2025-10-11 19:39:52 -07:00
jeffvli 22504e9e84 simplify prerelease deletion 2025-10-11 19:28:16 -07:00
jeffvli 5fb2ae839f fix previous release parser 2025-10-11 19:23:05 -07:00
jeffvli 15b00910f3 rework nightly deploy again
- rename to beta
- autodelete previous beta releases
- rename release title to Beta
2025-10-11 19:09:56 -07:00
jeffvli 6cce72a22a rework nightly deploy
- rename to development
- only manual push
- allow input for semantic version number
- set release to github prerelease instead of draft
2025-10-11 19:02:45 -07:00
jeffvli d48fe81d7f re-add build in nightly 2025-10-11 17:14:04 -07:00
jeffvli f0d0f826fb remove duplicate build in nightly 2025-10-11 15:06:52 -07:00
jeffvli 4d12a4d6cb add release channel setting and implementation 2025-10-11 15:05:29 -07:00
jeffvli f14d1f3c5c convert version bump to use pwsh 2025-10-11 14:15:03 -07:00
jeffvli cc466cb0f4 remove exemption for enhancements for stale issues 2025-10-11 13:26:43 -07:00
jeffvli 20941c0405 add initial nightly release workflow 2025-10-11 13:06:51 -07:00
Kendall Garner d52c067dc7 allow customizing windows install 2025-10-11 12:40:27 -07:00
Kendall Garner fccbf83c12 bugfix: handle playlist with no tracks 2025-10-11 12:39:59 -07:00
Kendall Garner 7817059a9e update serve image docs 2025-10-11 08:07:40 -07:00
Luis Canada d3a986e93c Add PWA to web app (#1175)
* add PWA to web app

* Fix sw.js not registering and lint

* Change sw and manifest to live at root

* Revert "Change sw and manifest to live at root"

This reverts commit 4c27d92467.
2025-10-11 14:12:25 +00:00
Kendall Garner 6733047942 improve jellyfin participants 2025-10-10 19:32:11 -07:00
Kendall Garner 452803fc72 support artist art as artist background 2025-10-10 18:26:28 -07:00
jeffvli 4e4a0464d6 pin pnpm/action-setup to v4.1.0 2025-10-10 12:36:08 -07:00
Kendall Garner 4ff317eac9 fix nonexistent filter 2025-10-05 21:25:19 -07:00
Kendall Garner 306167fee3 playlist sort and refactoring 2025-10-05 19:13:35 -07:00
Kendall Garner 1cbb3e56bc add recently released to home page, refactor home route 2025-10-05 07:51:36 -07:00
Kendall Garner 7c24f7cba4 use margin bottom for notifications component to not disable center controls 2025-10-04 07:34:48 -07:00
Evelyn Gravett 1b278cb33a Feature: Add song and artist links to discord RPC (#1160)
* Add song and artist links to discord RPC

* use first artist name for artist link, full artist name for song link

* use first album artist for song link

* add discord rpc links setting

* simplify discord link settings

* fix setting description

* add musicbrainz links

* fix callback missing dependency

* use encodeURIComponent for lastfm links

Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>

* split musicbrainz ids

* combine link settings

---------

Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
2025-10-04 03:27:59 +00:00
Kendall Garner f1a75d8e81 allow zero warnings on lint 2025-09-30 07:58:57 -07:00
Kendall Garner 4a48598260 add multiple genre support for nd albums/tracks 2025-09-28 19:59:20 -07:00
Kendall Garner 6df270ba34 server add/edit refactor, allow jellyfin prefer instant mix 2025-09-28 19:19:24 -07:00
Kendall Garner eb0ccec0bc Remove cached queries on editing server 2025-09-28 19:10:47 -07:00
Kendall Garner 8caf898172 have default background for artist top songs 2025-09-28 17:12:34 -07:00
Kendall Garner 508013958f ND >= 0.56.0: search songs by artist | album artist id 2025-09-27 20:00:34 -07:00
Kendall Garner c448352ec8 fix linter error 2025-09-26 17:30:20 -07:00
Henry e344adfeed Add autodiscovery for Jellyfin servers (#1146)
* Add autodiscovery for Jellyfin servers

* Remove debugging aids

you didn't see anything

* Fix linter errors

* Send a discovery packet to localhost too
2025-09-26 22:53:19 +00:00
Jeff bca4a14f2e adjust web playback error handler (#1150) 2025-09-24 18:09:30 -07:00
Kendall Garner f4be797f16 Add comment describing jellyfin image tag invalidation 2025-09-24 08:12:00 -07:00
Kendall Garner 2feef206fb add Navidrome/Jellyfin image cache invalidation 2025-09-24 08:05:22 -07:00
dependabot[bot] eea36f720a Bump axios in the npm_and_yarn group across 1 directory (#1145)
Bumps the npm_and_yarn group with 1 update in the / directory: [axios](https://github.com/axios/axios).


Updates `axios` from 1.9.0 to 1.12.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.9.0...v1.12.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.12.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-23 13:25:52 -07:00
dependabot[bot] 76350ed5af Bump vite in the npm_and_yarn group across 1 directory (#1115)
Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.3.5 to 6.3.6
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-23 12:51:06 -07:00
Gabriele Mancini 4f38e16857 Feature: added playlist duration badge (#1130) 2025-09-23 12:45:08 -07:00
Malachi Soord 8a3edb71df feat: add semantic selectors for now-playing media (#1138)
* feat: add semantic selectors for now-playing media

This change adds unique class names to the elements that display the currently playing media information. This makes it easier for extension developers to parse the DOM and understand what media is playing.

The following classes have been added:
- `media-player`: The main player container.
- `player-cover-art`: The cover art of the playing track.
- `song-title`: The title of the playing track.
- `song-artist`: The artist of the playing track.
- `song-album`: The album of the playing track.
- `player-state-playing`/`player-state-paused`: The state of the player.
- `elapsed-time`: The elapsed time of the playing track.
- `total-duration`: The total duration of the playing track.

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-09-23 12:44:22 -07:00
jeffvli 55e35e9b24 set default body background to #000 2025-09-22 18:24:04 -07:00
Gabriele Mancini 6abdbd2f3e Feature: added silent song notification setting (#1129)
* feat: added silent song notification
2025-09-17 21:06:59 -07:00
Kendall Garner 1d46cd5ff9 client-side only sort for all playlists (#1125)
* initial client-side only sort for all playlists

* allow reordering jellyfin (assume it works properly) and navidrome

* on playlist page, add to queue by sort order
2025-09-17 21:06:30 -07:00
Kendall Garner d68165dab5 move title to default layout 2025-09-15 20:10:56 -07:00
Kendall Garner dad80adb8b raise window on mpris raise 2025-09-15 19:31:10 -07:00
jeffvli 4134af0340 update to v0.20.1 2025-09-10 21:28:13 -07:00
Hosted Weblate 29a43ca185 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (699 of 699 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
2025-09-11 05:49:32 +02:00
Hosted Weblate e452f86170 Translated using Weblate (Slovak)
Currently translated at 100.0% (699 of 699 strings)

Translated using Weblate (Slovak)

Currently translated at 81.2% (568 of 699 strings)

Translated using Weblate (Slovak)

Currently translated at 78.3% (548 of 699 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: peter cerny <posli.to.semka@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/sk/
Translation: feishin/Translation
2025-09-11 05:49:32 +02:00
Hosted Weblate ec765dca6a Translated using Weblate (French)
Currently translated at 100.0% (699 of 699 strings)

Translated using Weblate (French)

Currently translated at 100.0% (699 of 699 strings)

Co-authored-by: Dylan MONTIGAUD <dylanmontigaud17@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translation: feishin/Translation
2025-09-11 05:49:31 +02:00
Hosted Weblate 64a3752b54 Translated using Weblate (Italian)
Currently translated at 100.0% (699 of 699 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Marco Ciotola <github@ciotola.dev>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/it/
Translation: feishin/Translation
2025-09-11 05:49:30 +02:00
Hosted Weblate 24069d285f Translated using Weblate (Czech)
Currently translated at 100.0% (699 of 699 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
2025-09-11 05:49:30 +02:00
Hosted Weblate 77fe886da4 Translated using Weblate (Catalan)
Currently translated at 100.0% (699 of 699 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ondo <SparkyOndo@proton.me>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ca/
Translation: feishin/Translation
2025-09-11 05:49:29 +02:00
Hosted Weblate ef16e1403d Translated using Weblate (Turkish)
Currently translated at 100.0% (699 of 699 strings)

Co-authored-by: Mücahit Kaya <kaya-mucahit@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/tr/
Translation: feishin/Translation
2025-09-11 05:49:28 +02:00
jeffvli ff6dda7b06 prevent width overflow on lyrics container (#1106) 2025-09-10 20:49:15 -07:00
Jeff 479aa2e22d Merge pull request #1122 from jeffvli/properly-handle-context-menu-close
fix(context menu): Properly handle click outside, and show initial rating
2025-09-10 20:39:31 -07:00
jeffvli ab8c3ad0ec handle initial rating for multiple items in context menu 2025-09-10 20:32:34 -07:00
Kendall Garner dc03a432fe add initial rating when a single item is provided in context menu 2025-09-10 20:20:56 -07:00
Kendall Garner 751ad55d02 remove all node selectors in useClickOutside 2025-09-10 17:49:13 -07:00
Kendall Garner 78dc89303d show right control rating for subsonic servers 2025-09-10 17:27:07 -07:00
Jeff 4328d8860e Merge pull request #1113 from maximelafarie/fix/darwin-top-bar
fix: electron menu bar for darwin devices
2025-09-10 00:43:59 -07:00
Maxime LAFARIE 58a36b3bba fix: electron menu bar for darwin 2025-09-10 09:36:19 +02:00
Jeff 618e5d8da8 Merge pull request #1114 from maximelafarie/feature/one-click-context-menu
feat: add context menu on left controls and sidebar image
2025-09-09 19:19:09 -07:00
Jeff be6ec49cfa Merge pull request #1107 from mihawk90/now-playing-reformat
cleanup notification text
2025-09-09 19:00:19 -07:00
Maxime LAFARIE 65ecdc7666 feat: add context menu on left controls and sidebar image 2025-09-09 23:38:39 +02:00
Tarulia da42fd78d2 cleanup notification text 2025-09-08 17:20:03 +02:00
273 changed files with 7470 additions and 15806 deletions
+329
View File
@@ -0,0 +1,329 @@
name: Publish Beta (Manual)
on:
workflow_dispatch:
inputs:
version:
description: 'Semantic version number (e.g., 1.0.0) - beta suffix will be added automatically'
required: false
type: string
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout git repo
uses: actions/checkout@v1
- name: Install Node and PNPM
uses: pnpm/action-setup@v4.1.0
with:
version: 9
- name: Install dependencies
run: pnpm install
- name: Validate and set version with beta suffix
id: version
shell: pwsh
run: |
$inputVersion = "${{ github.event.inputs.version }}"
Write-Host "Input version: $inputVersion"
if ($inputVersion -eq "" -or $inputVersion -eq "null") {
# No input version provided, auto-increment patch version
Write-Host "No version provided, auto-incrementing patch version..."
# Get current version from package.json
$currentVersion = (Get-Content package.json | ConvertFrom-Json).version
Write-Host "Current version: $currentVersion"
# Remove any existing suffix (like -beta) to get clean semantic version
$cleanVersion = $currentVersion -replace '-.*$', ''
# Extract major, minor, patch components
$versionParts = $cleanVersion.Split('.')
if ($versionParts.Length -ne 3) {
Write-Error "Current version format is invalid: $cleanVersion"
exit 1
}
$major = [int]$versionParts[0]
$minor = [int]$versionParts[1]
$patch = [int]$versionParts[2]
# Increment patch version
$newPatch = $patch + 1
$inputVersion = "$major.$minor.$newPatch"
Write-Host "Auto-generated version: $inputVersion"
} else {
# Validate semantic version format (major.minor.patch)
$versionPattern = '^\d+\.\d+\.\d+$'
if ($inputVersion -notmatch $versionPattern) {
Write-Error "Invalid version format. Expected semantic version (e.g., 1.0.0), got: $inputVersion"
exit 1
}
}
# Add beta suffix
$versionWithBeta = "$inputVersion-beta"
Write-Host "Setting version to: $versionWithBeta"
# Update package.json
$packageJson = Get-Content package.json | ConvertFrom-Json
$packageJson.version = $versionWithBeta
$packageJson | ConvertTo-Json -Depth 10 | Set-Content package.json
Write-Host "Updated package.json version to: $versionWithBeta"
# Set output for other jobs
echo "version=$versionWithBeta" >> $env:GITHUB_OUTPUT
- name: Delete existing releases and tags
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get the version that was set in the previous step
$versionWithBeta = "${{ steps.version.outputs.version }}"
Write-Host "Checking for existing releases with tag: $versionWithBeta"
# Find and delete any releases with isPrerelease "true"
Write-Host "Deleting existing prereleases..."
Write-Host "Searching for releases with isPrerelease 'true'..."
$betaReleases = gh release list --limit 100 --json tagName,isPrerelease,name | ConvertFrom-Json | Where-Object { $_.isPrerelease -eq $true }
if ($betaReleases) {
Write-Host "Found $($betaReleases.Count) release(s) with isPrerelease 'true':"
foreach ($release in $betaReleases) {
Write-Host " - Tag: $($release.tagName), Title: $($release.name)"
gh release delete $release.tagName --yes --cleanup-tag
Write-Host " Deleted release with tag: $($release.tagName)"
}
} else {
Write-Host "No releases found with isPrerelease 'true'"
}
publish:
needs: prepare
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
steps:
- name: Checkout git repo
uses: actions/checkout@v1
- name: Install Node and PNPM
uses: pnpm/action-setup@v4.1.0
with:
version: 9
- name: Install dependencies
run: pnpm install
- name: Set version from prepare job
shell: pwsh
run: |
$versionWithBeta = "${{ needs.prepare.outputs.version }}"
Write-Host "Setting version from prepare job: $versionWithBeta"
# Update package.json with the version from prepare job
$packageJson = Get-Content package.json | ConvertFrom-Json
$packageJson.version = $versionWithBeta
$packageJson | ConvertTo-Json -Depth 10 | Set-Content package.json
Write-Host "Updated package.json version to: $versionWithBeta"
- name: Build and Publish releases (Windows)
if: matrix.os == 'windows-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EP_PRE_RELEASE: true
uses: nick-invision/retry@v2.8.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:win:beta
on_retry_command: pnpm cache delete
- name: Build and Publish releases (macOS)
if: matrix.os == 'macos-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EP_PRE_RELEASE: true
uses: nick-invision/retry@v2.8.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:mac:beta
on_retry_command: pnpm cache delete
- name: Build and Publish releases (Linux)
if: matrix.os == 'ubuntu-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EP_PRE_RELEASE: true
uses: nick-invision/retry@v2.8.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:linux:beta
on_retry_command: pnpm cache delete
- name: Build and Publish releases (Linux ARM64)
if: matrix.os == 'ubuntu-latest'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EP_PRE_RELEASE: true
uses: nick-invision/retry@v2.8.2
with:
timeout_minutes: 30
max_attempts: 3
retry_on: error
command: |
pnpm run publish:linux-arm64:beta
on_retry_command: pnpm cache delete
edit-release:
needs: [prepare, publish]
runs-on: ubuntu-latest
steps:
- name: Checkout git repo
uses: actions/checkout@v1
- name: Edit release with commits and title
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get the version from the prepare job
$versionWithBeta = "${{ needs.prepare.outputs.version }}"
$tagVersion = "v" + $versionWithBeta
Write-Host "Editing release for tag: $tagVersion"
# Check if release exists
$releaseExists = gh release view $tagVersion 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Host "Found release with tag $tagVersion"
# Get current release notes
# Find the latest non-prerelease tag
Write-Host "Finding latest non-prerelease tag..."
$latestNonPrerelease = gh release list --limit 100 --json tagName,isPrerelease | ConvertFrom-Json | Where-Object { $_.isPrerelease -eq $false } | Select-Object -First 1
if ($latestNonPrerelease) {
$latestTag = $latestNonPrerelease.tagName
Write-Host "Latest non-prerelease tag: $latestTag"
# Get commits between latest non-prerelease and current HEAD
Write-Host "Getting commits between $latestTag and HEAD..."
# Use proper git range syntax and handle PowerShell string interpolation
$gitRange = "$latestTag..HEAD"
Write-Host "Git range: $gitRange"
# Get commits using proper git command with datetime
$commits = git log --oneline --pretty=format:"%ad|%s|%h" --date=short $gitRange
# Check if commits exist
if ($commits -and $commits.Trim() -ne "") {
Write-Host "Found commits:"
Write-Host $commits
# Group commits by date
$groupedCommits = @{}
foreach ($line in $commits) {
if ($line.Trim() -ne "") {
$parts = $line.Split('|')
$date = $parts[0]
$message = $parts[1]
$hash = $parts[2]
if (-not $groupedCommits.ContainsKey($date)) {
$groupedCommits[$date] = @()
}
$groupedCommits[$date] += "- $message ($hash)"
}
}
# Build formatted release notes grouped by date
$commitNotes = "## Changes since $latestTag`n`n"
$sortedDates = $groupedCommits.Keys | Sort-Object -Descending
foreach ($date in $sortedDates) {
$commitNotes += "### $date`n"
foreach ($commit in $groupedCommits[$date]) {
$commitNotes += "$commit`n"
}
$commitNotes += "`n"
}
$releaseNotes = $commitNotes
} else {
Write-Host "No commits found between $latestTag and HEAD"
Write-Host "Trying alternative approach..."
# Alternative: get commits since the tag (not range) with datetime
$commits = git log --oneline --pretty=format:"%ad|%s|%h" --date=short $latestTag.. --not $latestTag
if ($commits -and $commits.Trim() -ne "") {
Write-Host "Found commits with alternative method:"
Write-Host $commits
# Group commits by date
$groupedCommits = @{}
foreach ($line in $commits) {
if ($line.Trim() -ne "") {
$parts = $line.Split('|')
$date = $parts[0]
$message = $parts[1]
$hash = $parts[2]
if (-not $groupedCommits.ContainsKey($date)) {
$groupedCommits[$date] = @()
}
$groupedCommits[$date] += "- $message ($hash)"
}
}
# Build formatted release notes grouped by date
$commitNotes = "## Changes since $latestTag`n`n"
$sortedDates = $groupedCommits.Keys | Sort-Object -Descending
foreach ($date in $sortedDates) {
$commitNotes += "### $date`n"
foreach ($commit in $groupedCommits[$date]) {
$commitNotes += "$commit`n"
}
$commitNotes += "`n"
}
$releaseNotes = $commitNotes
} else {
Write-Host "Still no commits found, using basic release notes"
$releaseNotes = "## Beta Release`n`nThis is a beta release."
}
}
} else {
Write-Host "No non-prerelease tags found, using basic release notes"
$releaseNotes = "## Beta Release`n`nThis is a beta release."
}
# Update the release with new title and notes
Write-Host "Updating release with title 'Beta' and new notes..."
gh release edit $tagVersion --title "Beta" --notes "$releaseNotes"
Write-Host "Successfully updated release title to 'Beta' and added commit notes"
} else {
Write-Host "No release found with tag $tagVersion"
}
+1 -3
View File
@@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v1 uses: actions/checkout@v1
- name: Install Node and PNPM - name: Install Node and PNPM
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4.1.0
with: with:
version: 9 version: 9
@@ -31,7 +31,6 @@ jobs:
max_attempts: 3 max_attempts: 3
retry_on: error retry_on: error
command: | command: |
pnpm run package:linux
pnpm run publish:linux pnpm run publish:linux
on_retry_command: pnpm cache delete on_retry_command: pnpm cache delete
@@ -44,6 +43,5 @@ jobs:
max_attempts: 3 max_attempts: 3
retry_on: error retry_on: error
command: | command: |
pnpm run package:linux-arm64
pnpm run publish:linux-arm64 pnpm run publish:linux-arm64
on_retry_command: pnpm cache delete on_retry_command: pnpm cache delete
+1 -2
View File
@@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v1 uses: actions/checkout@v1
- name: Install Node and PNPM - name: Install Node and PNPM
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4.1.0
with: with:
version: 9 version: 9
@@ -31,6 +31,5 @@ jobs:
max_attempts: 3 max_attempts: 3
retry_on: error retry_on: error
command: | command: |
pnpm run package:mac
pnpm run publish:mac pnpm run publish:mac
on_retry_command: pnpm cache delete on_retry_command: pnpm cache delete
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install Node and PNPM - name: Install Node and PNPM
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4.1.0
with: with:
version: 9 version: 9
+1 -2
View File
@@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v1 uses: actions/checkout@v1
- name: Install Node and PNPM - name: Install Node and PNPM
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4.1.0
with: with:
version: 9 version: 9
@@ -31,6 +31,5 @@ jobs:
max_attempts: 3 max_attempts: 3
retry_on: error retry_on: error
command: | command: |
pnpm run package:win
pnpm run publish:win pnpm run publish:win
on_retry_command: pnpm cache delete on_retry_command: pnpm cache delete
+1 -1
View File
@@ -42,6 +42,6 @@ jobs:
stale-issue-label: 'stale' stale-issue-label: 'stale'
exempt-issue-labels: 'enhancement,keep,security' exempt-issue-labels: 'keep,security'
stale-pr-label: 'stale' stale-pr-label: 'stale'
exempt-pr-labels: 'keep,security' exempt-pr-labels: 'keep,security'
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v1 uses: actions/checkout@v1
- name: Install Node.js and PNPM - name: Install Node.js and PNPM
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4.1.0
with: with:
version: 9 version: 9
+8 -4
View File
@@ -157,14 +157,18 @@ This project is built off of [electron-vite](https://github.com/alex8088/electro
- `pnpm run build:remote` - Build the remote app (remote) - `pnpm run build:remote` - Build the remote app (remote)
- `pnpm run build:web` - Build the standalone web app (renderer) - `pnpm run build:web` - Build the standalone web app (renderer)
- `pnpm run package` - Package the project - `pnpm run package` - Package the project
- `pnpm run package:dev` - Package the project for development - `pnpm run package:dev` - Package the project for development locally
- `pnpm run package:linux` - Package the project for Linux - `pnpm run package:linux` - Package the project for Linux locally
- `pnpm run package:mac` - Package the project for Mac - `pnpm run package:mac` - Package the project for Mac locally
- `pnpm run package:win` - Package the project for Windows - `pnpm run package:win` - Package the project for Windows locally
- `pnpm run publish:linux` - Publish the project for Linux - `pnpm run publish:linux` - Publish the project for Linux
- `pnpm run publish:linux:beta` - Publish the project for Linux (beta channel)
- `pnpm run publish:linux-arm64` - Publish the project for Linux ARM64 - `pnpm run publish:linux-arm64` - Publish the project for Linux ARM64
- `pnpm run publish:linux-arm64:beta` - Publish the project for Linux ARM64 (beta channel)
- `pnpm run publish:mac` - Publish the project for Mac - `pnpm run publish:mac` - Publish the project for Mac
- `pnpm run publish:mac:beta` - Publish the project for Mac (beta channel)
- `pnpm run publish:win` - Publish the project for Windows - `pnpm run publish:win` - Publish the project for Windows
- `pnpm run publish:win:beta` - Publish the project for Windows (beta channel)
- `pnpm run typecheck` - Type check the project - `pnpm run typecheck` - Type check the project
- `pnpm run typecheck:node` - Type check the project with tsconfig.node.json - `pnpm run typecheck:node` - Type check the project with tsconfig.node.json
- `pnpm run typecheck:web` - Type check the project with tsconfig.web.json - `pnpm run typecheck:web` - Type check the project with tsconfig.web.json
+57
View File
@@ -0,0 +1,57 @@
appId: org.jeffvli.feishin
productName: Feishin
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
electronVersion: 35.1.5
directories:
buildResources: assets
files:
- 'out/**/*'
- 'package.json'
extraResources:
- assets/**
asarUnpack:
- resources/**
win:
target:
- zip
- nsis
icon: assets/icons/icon.png
nsis:
allowToChangeInstallationDirectory: true
oneClick: false
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
target:
target: default
arch:
- arm64
- x64
icon: assets/icons/icon.icns
type: distribution
hardenedRuntime: true
entitlements: assets/entitlements.mac.plist
entitlementsInherit: assets/entitlements.mac.plist
gatekeeperAssess: false
notarize: false
dmg:
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
linux:
target:
- AppImage
- tar.xz
category: AudioVideo;Audio;Player
icon: assets/icons/icon.png
artifactName: ${productName}-${os}-${arch}.${ext}
npmRebuild: false
publish:
provider: github
owner: jeffvli
repo: feishin
channel: beta
+8
View File
@@ -16,10 +16,14 @@ win:
- zip - zip
- nsis - nsis
icon: assets/icons/icon.png icon: assets/icons/icon.png
nsis: nsis:
allowToChangeInstallationDirectory: true
oneClick: false
shortcutName: ${productName} shortcutName: ${productName}
uninstallDisplayName: ${productName} uninstallDisplayName: ${productName}
createDesktopShortcut: always createDesktopShortcut: always
mac: mac:
target: target:
target: default target: default
@@ -33,8 +37,10 @@ mac:
entitlementsInherit: assets/entitlements.mac.plist entitlementsInherit: assets/entitlements.mac.plist
gatekeeperAssess: false gatekeeperAssess: false
notarize: false notarize: false
dmg: dmg:
contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }] contents: [{ x: 130, y: 220 }, { x: 410, y: 220, type: link, path: /Applications }]
linux: linux:
target: target:
- AppImage - AppImage
@@ -42,8 +48,10 @@ linux:
category: AudioVideo;Audio;Player category: AudioVideo;Audio;Player
icon: assets/icons/icon.png icon: assets/icons/icon.png
artifactName: ${productName}-${os}-${arch}.${ext} artifactName: ${productName}-${os}-${arch}.${ext}
npmRebuild: false npmRebuild: false
publish: publish:
provider: github provider: github
owner: jeffvli owner: jeffvli
repo: feishin repo: feishin
channel: latest
-2
View File
@@ -30,7 +30,6 @@ const config: UserConfig = {
], ],
resolve: { resolve: {
alias: { alias: {
'/@/i18n': resolve('src/i18n'),
'/@/main': resolve('src/main'), '/@/main': resolve('src/main'),
'/@/shared': resolve('src/shared'), '/@/shared': resolve('src/shared'),
}, },
@@ -40,7 +39,6 @@ const config: UserConfig = {
plugins: [externalizeDepsPlugin()], plugins: [externalizeDepsPlugin()],
resolve: { resolve: {
alias: { alias: {
'/@/i18n': resolve('src/i18n'),
'/@/preload': resolve('src/preload'), '/@/preload': resolve('src/preload'),
'/@/shared': resolve('src/shared'), '/@/shared': resolve('src/shared'),
}, },
+1 -1
View File
@@ -6,7 +6,7 @@ import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
import eslintPluginReactRefresh from 'eslint-plugin-react-refresh'; import eslintPluginReactRefresh from 'eslint-plugin-react-refresh';
export default tseslint.config( export default tseslint.config(
{ ignores: ['**/node_modules', '**/dist', '**/out', '**/*-schema.d.ts'] }, { ignores: ['**/node_modules', '**/dist', '**/out'] },
tseslint.configs.recommended, tseslint.configs.recommended,
perfectionist.configs['recommended-natural'], perfectionist.configs['recommended-natural'],
eslintPluginReact.configs.flat.recommended, eslintPluginReact.configs.flat.recommended,
+18 -18
View File
@@ -1,6 +1,6 @@
{ {
"name": "feishin", "name": "feishin",
"version": "0.20.0", "version": "0.21.0",
"description": "A modern self-hosted music player.", "description": "A modern self-hosted music player.",
"keywords": [ "keywords": [
"subsonic", "subsonic",
@@ -27,14 +27,12 @@
"dev": "electron-vite dev", "dev": "electron-vite dev",
"dev:remote": "vite dev --config remote.vite.config.ts", "dev:remote": "vite dev --config remote.vite.config.ts",
"dev:watch": "electron-vite dev --watch", "dev:watch": "electron-vite dev --watch",
"generate-api": "pnpm run generate-api:subsonic",
"generate-api:subsonic": "openapi-typescript https://opensubsonic.netlify.app/docs/openapi/openapi.json -o ./src/shared/api/subsonic/subsonic-schema.d.ts",
"i18next": "i18next -c src/i18n/i18next-parser.config.js", "i18next": "i18next -c src/i18n/i18next-parser.config.js",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"lint": "pnpm run lint-code && pnpm run lint-styles", "lint": "pnpm run lint-code && pnpm run lint-styles",
"lint-code": "eslint --cache .", "lint-code": "eslint --max-warnings=0 --cache .",
"lint-code:fix": "eslint --cache --fix .", "lint-code:fix": "eslint --cache --fix .",
"lint-styles": "stylelint 'src/**/*.{css,scss}'", "lint-styles": "stylelint --max-warnings=0 'src/**/*.{css,scss}'",
"lint-styles:fix": "stylelint 'src/**/*.{css,scss}' --fix", "lint-styles:fix": "stylelint 'src/**/*.{css,scss}' --fix",
"lint:fix": "pnpm run lint-code:fix && pnpm run lint-styles:fix", "lint:fix": "pnpm run lint-code:fix && pnpm run lint-styles:fix",
"package": "pnpm run build && electron-builder", "package": "pnpm run build && electron-builder",
@@ -46,10 +44,14 @@
"package:mac:pr": "pnpm run build && electron-builder --mac --publish never", "package:mac:pr": "pnpm run build && electron-builder --mac --publish never",
"package:win": "pnpm run build && electron-builder --win", "package:win": "pnpm run build && electron-builder --win",
"package:win:pr": "pnpm run build && electron-builder --win --publish never", "package:win:pr": "pnpm run build && electron-builder --win --publish never",
"publish:linux": "electron-builder --publish always --linux", "publish:linux": "pnpm run build && electron-builder --publish always --linux",
"publish:linux-arm64": "electron-builder --publish always --linux --arm64", "publish:linux-arm64": "pnpm run build && electron-builder --publish always --linux --arm64",
"publish:mac": "electron-builder --publish always --mac", "publish:linux-arm64:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --linux --arm64",
"publish:win": "electron-builder --publish always --win", "publish:linux:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --linux",
"publish:mac": "pnpm run build && electron-builder --publish always --mac",
"publish:mac:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --mac",
"publish:win": "pnpm run build && electron-builder --publish always --win",
"publish:win:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win",
"start": "electron-vite preview", "start": "electron-vite preview",
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web", "typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
@@ -73,14 +75,14 @@
"@mantine/hooks": "^8.2.8", "@mantine/hooks": "^8.2.8",
"@mantine/modals": "^8.2.8", "@mantine/modals": "^8.2.8",
"@mantine/notifications": "^8.2.8", "@mantine/notifications": "^8.2.8",
"@tanstack/react-query": "^5.83.0", "@tanstack/react-query": "^4.32.1",
"@tanstack/react-query-devtools": "^5.83.0", "@tanstack/react-query-devtools": "^4.32.1",
"@tanstack/react-query-persist-client": "^5.83.0", "@tanstack/react-query-persist-client": "^4.32.1",
"@ts-rest/core": "^3.23.0", "@ts-rest/core": "^3.23.0",
"@xhayper/discord-rpc": "^1.3.0", "@xhayper/discord-rpc": "^1.3.0",
"audiomotion-analyzer": "^4.5.0", "audiomotion-analyzer": "^4.5.0",
"auto-text-size": "^0.2.3", "auto-text-size": "^0.2.3",
"axios": "^1.6.0", "axios": "^1.12.0",
"cheerio": "^1.0.0", "cheerio": "^1.0.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"cmdk": "^0.2.0", "cmdk": "^0.2.0",
@@ -105,7 +107,6 @@
"mpris-service": "^2.1.2", "mpris-service": "^2.1.2",
"nanoid": "^3.3.3", "nanoid": "^3.3.3",
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f", "node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
"openapi-fetch": "^0.14.0",
"overlayscrollbars": "^2.11.1", "overlayscrollbars": "^2.11.1",
"overlayscrollbars-react": "^0.5.6", "overlayscrollbars-react": "^0.5.6",
"qs": "^6.14.0", "qs": "^6.14.0",
@@ -138,7 +139,6 @@
"@types/lodash": "^4.17.18", "@types/lodash": "^4.17.18",
"@types/md5": "^2.3.5", "@types/md5": "^2.3.5",
"@types/node": "^22.15.32", "@types/node": "^22.15.32",
"@types/qs": "^6.14.0",
"@types/react": "^18.3.23", "@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7", "@types/react-dom": "^18.3.7",
"@types/react-window": "^1.8.5", "@types/react-window": "^1.8.5",
@@ -159,7 +159,6 @@
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.19",
"i18next-parser": "^9.0.2", "i18next-parser": "^9.0.2",
"openapi-typescript": "^7.8.0",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-packagejson": "^2.5.14", "prettier-plugin-packagejson": "^2.5.14",
@@ -169,10 +168,11 @@
"stylelint-config-recess-order": "^7.1.0", "stylelint-config-recess-order": "^7.1.0",
"stylelint-config-standard": "^38.0.0", "stylelint-config-standard": "^38.0.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.3.5", "vite": "^6.3.6",
"vite-plugin-conditional-import": "^0.1.7", "vite-plugin-conditional-import": "^0.1.7",
"vite-plugin-dynamic-import": "^1.6.0", "vite-plugin-dynamic-import": "^1.6.0",
"vite-plugin-ejs": "^1.7.0" "vite-plugin-ejs": "^1.7.0",
"vite-plugin-pwa": "^1.0.3"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
+2343 -393
View File
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -1,7 +1,5 @@
module.exports = { module.exports = {
plugins: { plugins: {
'postcss-preset-mantine': { 'postcss-preset-mantine': {},
mixins: {},
},
}, },
}; };
+7 -2
View File
@@ -75,7 +75,9 @@
"download": "descarregar", "download": "descarregar",
"showDetails": "informació", "showDetails": "informació",
"numberSelected": "{{count}} seleccionat", "numberSelected": "{{count}} seleccionat",
"shareItem": "comparteix l'element" "shareItem": "comparteix l'element",
"goToAlbumArtist": "Ves a $t(entity.albumArtist_one)",
"goToAlbum": "ves a $t(entity.album_one)"
}, },
"genreList": { "genreList": {
"title": "$t(entity.genre_other)", "title": "$t(entity.genre_other)",
@@ -641,7 +643,10 @@
"discordDisplayType": "tipus de pantalla d'activitat de {{discord}}", "discordDisplayType": "tipus de pantalla d'activitat de {{discord}}",
"discordDisplayType_description": "canvia què escolteu al vostre estat", "discordDisplayType_description": "canvia què escolteu al vostre estat",
"discordDisplayType_songname": "nom de la cançó", "discordDisplayType_songname": "nom de la cançó",
"discordDisplayType_artistname": "nom de l'artista" "discordDisplayType_artistname": "nom de l'artista",
"hotkey_navigateHome": "ves a l'inici",
"preventSleepOnPlayback": "evitar entrar en repòs durant la reproducció",
"preventSleepOnPlayback_description": "evita que la pantalla s'adormi mentre la música es reprodueix"
}, },
"table": { "table": {
"column": { "column": {
+7 -2
View File
@@ -277,7 +277,10 @@
"discordDisplayType": "typ zobrazení stavu {{discord}}", "discordDisplayType": "typ zobrazení stavu {{discord}}",
"discordDisplayType_description": "změní, co posloucháte, ve vašem stavu", "discordDisplayType_description": "změní, co posloucháte, ve vašem stavu",
"discordDisplayType_songname": "název skladby", "discordDisplayType_songname": "název skladby",
"discordDisplayType_artistname": "jména umělců" "discordDisplayType_artistname": "jména umělců",
"hotkey_navigateHome": "přejít domů",
"preventSleepOnPlayback": "zabránit uspání při přehrávání",
"preventSleepOnPlayback_description": "zabránit uspání displeje během přehrávání hudby"
}, },
"action": { "action": {
"editPlaylist": "upravit $t(entity.playlist_one)", "editPlaylist": "upravit $t(entity.playlist_one)",
@@ -625,7 +628,9 @@
"playSimilarSongs": "$t(player.playSimilarSongs)", "playSimilarSongs": "$t(player.playSimilarSongs)",
"download": "stáhnout", "download": "stáhnout",
"playShuffled": "$t(player.shuffle)", "playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)" "moveToNext": "$t(action.moveToNext)",
"goToAlbum": "přejít na $t(entity.album_one)",
"goToAlbumArtist": "přejít na $t(entity.albumArtist_one)"
}, },
"home": { "home": {
"mostPlayed": "nejpřehrávanější", "mostPlayed": "nejpřehrávanější",
+17 -6
View File
@@ -227,11 +227,7 @@
"songCount": "song count", "songCount": "song count",
"title": "title", "title": "title",
"toYear": "to year", "toYear": "to year",
"trackNumber": "track", "trackNumber": "track"
"createdAt": "created at",
"updatedAt": "updated at",
"type": "type",
"email": "email"
}, },
"form": { "form": {
"addServer": { "addServer": {
@@ -241,6 +237,8 @@
"input_legacyAuthentication": "enable legacy authentication", "input_legacyAuthentication": "enable legacy authentication",
"input_name": "server name", "input_name": "server name",
"input_password": "password", "input_password": "password",
"input_preferInstantMix": "prefer instant mix",
"input_preferInstantMixDescription": "only use instant mix to get similar songs. useful if you have plugins that modify this behavior",
"input_savePassword": "save password", "input_savePassword": "save password",
"input_url": "url", "input_url": "url",
"input_username": "username", "input_username": "username",
@@ -413,6 +411,7 @@
"mostPlayed": "most played", "mostPlayed": "most played",
"newlyAdded": "newly added releases", "newlyAdded": "newly added releases",
"recentlyPlayed": "recently played", "recentlyPlayed": "recently played",
"recentlyReleased": "recently released",
"title": "$t(common.home)" "title": "$t(common.home)"
}, },
"itemDetail": { "itemDetail": {
@@ -498,6 +497,10 @@
"albumBackgroundBlur_description": "adjusts the amount of blur applied to the album background image", "albumBackgroundBlur_description": "adjusts the amount of blur applied to the album background image",
"applicationHotkeys": "application hotkeys", "applicationHotkeys": "application hotkeys",
"applicationHotkeys_description": "configure application hotkeys. toggle the checkbox to set as a global hotkey (desktop only)", "applicationHotkeys_description": "configure application hotkeys. toggle the checkbox to set as a global hotkey (desktop only)",
"artistBackground": "artist background image",
"artistBackground_description": "adds a background image for artist pages containing the artist art",
"artistBackgroundBlur": "artist background image blur size",
"artistBackgroundBlur_description": "adjusts the amount of blur applied to the artist background image",
"artistConfiguration": "album artist page configuration", "artistConfiguration": "album artist page configuration",
"artistConfiguration_description": "configure what items are shown, and in what order, on the album artist page", "artistConfiguration_description": "configure what items are shown, and in what order, on the album artist page",
"audioDevice": "audio device", "audioDevice": "audio device",
@@ -527,6 +530,10 @@
"customFontPath": "custom font path", "customFontPath": "custom font path",
"customFontPath_description": "sets the path to the custom font to use for the application", "customFontPath_description": "sets the path to the custom font to use for the application",
"disableAutomaticUpdates": "disable automatic updates", "disableAutomaticUpdates": "disable automatic updates",
"releaseChannel_optionLatest": "latest",
"releaseChannel_optionBeta": "beta",
"releaseChannel": "release channel",
"releaseChannel_description": "choose between stable releases or beta releases for automatic updates",
"disableLibraryUpdateOnStartup": "disable checking for new versions on startup", "disableLibraryUpdateOnStartup": "disable checking for new versions on startup",
"discordApplicationId": "{{discord}} application id", "discordApplicationId": "{{discord}} application id",
"discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})", "discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})",
@@ -539,13 +546,17 @@
"discordRichPresence": "{{discord}} rich presence", "discordRichPresence": "{{discord}} rich presence",
"discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}}", "discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}}",
"discordServeImage": "serve {{discord}} images from server", "discordServeImage": "serve {{discord}} images from server",
"discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for jellyfin and navidrome", "discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for jellyfin and navidrome. {{discord}} uses a bot to fetch images, so your server must be reachable from the public internet.",
"discordUpdateInterval": "{{discord}} rich presence update interval", "discordUpdateInterval": "{{discord}} rich presence update interval",
"discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)", "discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)",
"discordDisplayType": "{{discord}} presence display type", "discordDisplayType": "{{discord}} presence display type",
"discordDisplayType_description": "changes what you are listening to in your status", "discordDisplayType_description": "changes what you are listening to in your status",
"discordDisplayType_songname": "song name", "discordDisplayType_songname": "song name",
"discordDisplayType_artistname": "artist name(s)", "discordDisplayType_artistname": "artist name(s)",
"discordLinkType": "{{discord}} presence links",
"discordLinkType_description": "adds external links to {{lastfm}} or {{musicbrainz}} to the song and artist fields in {{discord}} rich presence. {{musicbrainz}} is the most accurate but requires tags and doesn't provide artist links while {{lastfm}} should always provide a link. makes no extra network requests",
"discordLinkType_none": "$t(common.none)",
"discordLinkType_mbz_lastfm": "{{musicbrainz}} with {{lastfm}} fallback",
"doubleClickBehavior": "queue all searched tracks when double clicking", "doubleClickBehavior": "queue all searched tracks when double clicking",
"doubleClickBehavior_description": "if true, all matching tracks in a track search will be queued. otherwise, only the clicked one will be queued", "doubleClickBehavior_description": "if true, all matching tracks in a track search will be queued. otherwise, only the clicked one will be queued",
"enableRemote": "enable remote control server", "enableRemote": "enable remote control server",
+10 -5
View File
@@ -243,7 +243,7 @@
"artists": "$t(entity.artist_other)", "artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)", "albumArtists": "$t(entity.albumArtist_other)",
"shared": "partagé $t(entity.playlist_other)", "shared": "partagé $t(entity.playlist_other)",
"myLibrary": "ma bibliothèque" "myLibrary": "Bibliothèque"
}, },
"fullscreenPlayer": { "fullscreenPlayer": {
"config": { "config": {
@@ -331,7 +331,9 @@
"showDetails": "obtenir des informations", "showDetails": "obtenir des informations",
"download": "télécharger", "download": "télécharger",
"playShuffled": "$t(player.shuffle)", "playShuffled": "$t(player.shuffle)",
"moveToNext": "$t(action.moveToNext)" "moveToNext": "$t(action.moveToNext)",
"goToAlbumArtist": "aller à l'$t(entity.albumArtist_one)",
"goToAlbum": "aller à l'$t(entity.album_one)"
}, },
"albumArtistList": { "albumArtistList": {
"title": "$t(entity.albumArtist_other)" "title": "$t(entity.albumArtist_other)"
@@ -499,7 +501,7 @@
"sidebarCollapsedNavigation_description": "affiche ou cache la navigation dans la barre latérale réduite", "sidebarCollapsedNavigation_description": "affiche ou cache la navigation dans la barre latérale réduite",
"sidebarConfiguration": "configuration de la barre latérale", "sidebarConfiguration": "configuration de la barre latérale",
"sidebarConfiguration_description": "sélectionnez les éléments et l'ordre dans lequel ils seront affichés dans la barre latérale", "sidebarConfiguration_description": "sélectionnez les éléments et l'ordre dans lequel ils seront affichés dans la barre latérale",
"sidebarPlaylistList": "liste de listes de lecture de la barre latérale", "sidebarPlaylistList": "liste des listes de lecture de la barre latérale",
"sidebarCollapsedNavigation": "navigation de la barre latéral (réduite)", "sidebarCollapsedNavigation": "navigation de la barre latéral (réduite)",
"skipDuration": "durée de l'avance rapide", "skipDuration": "durée de l'avance rapide",
"sidePlayQueueStyle_optionAttached": "attaché", "sidePlayQueueStyle_optionAttached": "attaché",
@@ -546,7 +548,7 @@
"clearQueryCache": "vide le cache de feishin", "clearQueryCache": "vide le cache de feishin",
"clearCache": "vider le cache navigateur", "clearCache": "vider le cache navigateur",
"buttonSize_description": "la taille des boutons de la barre de lecture", "buttonSize_description": "la taille des boutons de la barre de lecture",
"clearQueryCache_description": "un 'soft clear' de Feishin. Cela actualisera les liste de lecture, les métadonnées des titres, et réinitialisera les paroles enregistrées. Les paramètres, identifiants du serveur et images mises en cache seront conservés", "clearQueryCache_description": "un 'soft clear' de Feishin. cela actualisera les liste de lecture, les métadonnées des titres, et réinitialisera les paroles enregistrées. les paramètres, identifiants du serveur et images mises en cache seront conservés",
"clearCache_description": "un 'hard clear' de feishin. en plus de vider le cache de feishin, vide le cache du navigateur (images sauvegardées et autres ressources). les identifiants serveurs et paramètres sont conservés", "clearCache_description": "un 'hard clear' de feishin. en plus de vider le cache de feishin, vide le cache du navigateur (images sauvegardées et autres ressources). les identifiants serveurs et paramètres sont conservés",
"buttonSize": "taille des boutons du lecteur", "buttonSize": "taille des boutons du lecteur",
"clearCacheSuccess": "le cache a été vidé", "clearCacheSuccess": "le cache a été vidé",
@@ -627,7 +629,10 @@
"discordDisplayType": "type d'affichage du status {{discord}}", "discordDisplayType": "type d'affichage du status {{discord}}",
"discordDisplayType_description": "change ce que vous écoutez dans votre statut", "discordDisplayType_description": "change ce que vous écoutez dans votre statut",
"discordDisplayType_songname": "nom du morceau", "discordDisplayType_songname": "nom du morceau",
"discordDisplayType_artistname": "nom(s) dartiste" "discordDisplayType_artistname": "nom(s) dartiste",
"hotkey_navigateHome": "aller à l'accueil",
"preventSleepOnPlayback_description": "Empêche la mise en veille du lecteur lorsque la musique est en cours de lecture",
"preventSleepOnPlayback": "Empêche la mise en veille lors de la lecture"
}, },
"form": { "form": {
"deletePlaylist": { "deletePlaylist": {
+31 -10
View File
@@ -114,12 +114,14 @@
"codec": "codec", "codec": "codec",
"mbid": "MusicBrainz ID", "mbid": "MusicBrainz ID",
"preview": "anteprima", "preview": "anteprima",
"reload": "ricarica", "reload": "aggiorna",
"share": "condividi", "share": "condividi",
"tags": "tags", "tags": "tags",
"trackGain": "normalizzazione (gain) del brano", "trackGain": "normalizzazione (gain) del brano",
"trackPeak": "picco di volume del brano", "trackPeak": "picco di volume del brano",
"translation": "traduzione" "translation": "traduzione",
"bitDepth": "bit depth (profondità di bit)",
"sampleRate": "sample rate (frequenza di campionamento)"
}, },
"player": { "player": {
"repeat_all": "ripeti coda", "repeat_all": "ripeti coda",
@@ -231,7 +233,7 @@
"hotkey_toggleShuffle": "attiva/disattiva mescolamento", "hotkey_toggleShuffle": "attiva/disattiva mescolamento",
"theme": "tema", "theme": "tema",
"playbackStyle_description": "selezione lo stile di riproduzione da usare per il player audio", "playbackStyle_description": "selezione lo stile di riproduzione da usare per il player audio",
"discordRichPresence_description": "abilita lo status del playback nello stato attività di {{discord}}. Le chiavi immagine sono: {{icon}}, {{playing}} e {{paused}}", "discordRichPresence_description": "abilita lo stato di riproduzione nello stato attività di {{discord}}. Le chiavi immagine sono: {{icon}}, {{playing}} e {{paused}}",
"mpvExecutablePath": "percorso eseguibile mpv", "mpvExecutablePath": "percorso eseguibile mpv",
"audioDevice": "device audio", "audioDevice": "device audio",
"hotkey_rate2": "voto 2 stelle", "hotkey_rate2": "voto 2 stelle",
@@ -266,7 +268,7 @@
"customFontPath": "percorso font personalizzato", "customFontPath": "percorso font personalizzato",
"followLyric": "segui testo corrente", "followLyric": "segui testo corrente",
"crossfadeDuration": "durata dissolvenza", "crossfadeDuration": "durata dissolvenza",
"discordIdleStatus": "visualizza lo stato attività in stato inattivo", "discordIdleStatus": "mostra lo stato attività di Discord quando non stai riproducendo",
"audioPlayer": "player audio", "audioPlayer": "player audio",
"hotkey_zoomOut": "rimpicciolisci layout", "hotkey_zoomOut": "rimpicciolisci layout",
"hotkey_rate0": "rimuovi voto", "hotkey_rate0": "rimuovi voto",
@@ -331,12 +333,12 @@
"customCssNotice": "Attenzione: sebbene ci sia una certa sanitizzazione (vengono bloccati url() e content:), luso di CSS personalizzati può comunque comportare dei rischi modificando linterfaccia.", "customCssNotice": "Attenzione: sebbene ci sia una certa sanitizzazione (vengono bloccati url() e content:), luso di CSS personalizzati può comunque comportare dei rischi modificando linterfaccia.",
"customCss": "css personalizzato", "customCss": "css personalizzato",
"customCss_description": "contenuto CSS personalizzato. Nota: le proprietà content e gli URL remoti non sono consentiti. Di seguito è mostrata unanteprima del tuo contenuto. Sono presenti anche altri campi non impostati da te a causa della sanitizzazione.", "customCss_description": "contenuto CSS personalizzato. Nota: le proprietà content e gli URL remoti non sono consentiti. Di seguito è mostrata unanteprima del tuo contenuto. Sono presenti anche altri campi non impostati da te a causa della sanitizzazione.",
"discordPausedStatus": "mostra rich presence di Discord quando la riproduzione è in pausa", "discordPausedStatus": "mostra lo stato attività di Discord quando la riproduzione è in pausa",
"discordPausedStatus_description": "quando abilitato, verrà mostrato lo stato del lettore in standby/pausa (nessun brano in riproduzione)", "discordPausedStatus_description": "quando abilitato, verrà mostrato lo stato del lettore in standby/pausa (nessun brano in riproduzione)",
"discordListening": "mostra stato come in ascolto", "discordListening": "mostra stato come in ascolto",
"discordListening_description": "mostra lo stato come in ascolto invece che in riproduzione", "discordListening_description": "mostra lo stato come in ascolto invece che in riproduzione",
"discordServeImage": "recupera le immagini di {{discord}} dal server", "discordServeImage": "recupera le immagini di {{discord}} dal server",
"discordServeImage_description": "condividi la copertina per la rich presence di {{discord}} direttamente dal server, disponibile solo per Jellyfin e Navidrome", "discordServeImage_description": "condividi la copertina per lo stato attività di {{discord}} direttamente dal server, disponibile solo per Jellyfin e Navidrome",
"doubleClickBehavior": "aggiungi alla coda tutte le tracce cercate, con un doppio clic", "doubleClickBehavior": "aggiungi alla coda tutte le tracce cercate, con un doppio clic",
"doubleClickBehavior_description": "se attivato, tutte le tracce corrispondenti alla ricerca verranno aggiunte alla coda. altrimenti, verrà aggiunta alla coda solo la traccia selezionata", "doubleClickBehavior_description": "se attivato, tutte le tracce corrispondenti alla ricerca verranno aggiunte alla coda. altrimenti, verrà aggiunta alla coda solo la traccia selezionata",
"externalLinks": "mostra link esterni", "externalLinks": "mostra link esterni",
@@ -393,7 +395,16 @@
"webAudio_description": "usa audio web. abilita funzionalità avanzate come ReplayGain. disabilita se riscontri problemi", "webAudio_description": "usa audio web. abilita funzionalità avanzate come ReplayGain. disabilita se riscontri problemi",
"preservePitch": "mantieni tono (pitch)", "preservePitch": "mantieni tono (pitch)",
"preservePitch_description": "mantiene il tono (pitch) durante la modifica della velocità di riproduzione", "preservePitch_description": "mantiene il tono (pitch) durante la modifica della velocità di riproduzione",
"volumeWidth_description": "larghezza del cursore del volume" "volumeWidth_description": "larghezza del cursore del volume",
"discordDisplayType_description": "modifica cosa stai ascoltando nel tuo stato",
"discordDisplayType_songname": "titolo traccia",
"discordDisplayType_artistname": "nome artisti",
"hotkey_navigateHome": "vai alla schermata iniziale",
"notify": "abilita notifiche delle tracce",
"notify_description": "mostra una notifica quando cambia la traccia riprodotta",
"preventSleepOnPlayback": "non sospendere in riproduzione",
"preventSleepOnPlayback_description": "non sospendere il sistema quando la riproduzione è attiva",
"discordDisplayType": "stile dello stato su {{discord}}"
}, },
"error": { "error": {
"remotePortWarning": "riavvia il server per applicare la nuova porta", "remotePortWarning": "riavvia il server per applicare la nuova porta",
@@ -418,7 +429,8 @@
"badAlbum": "stai visualizzando questa pagina perché questa canzone non fa parte di un album. probabilmente vedi questo messaggio perché hai una canzone posizionata direttamente nella cartella principale della tua libreria musicale. jellyfin raggruppa le tracce solo se si trovano allinterno di una cartella.", "badAlbum": "stai visualizzando questa pagina perché questa canzone non fa parte di un album. probabilmente vedi questo messaggio perché hai una canzone posizionata direttamente nella cartella principale della tua libreria musicale. jellyfin raggruppa le tracce solo se si trovano allinterno di una cartella.",
"badValue": "opzione non valida \"{{value}}\". valore inesistente", "badValue": "opzione non valida \"{{value}}\". valore inesistente",
"networkError": "si è verificato un errore di rete", "networkError": "si è verificato un errore di rete",
"openError": "impossibile aprire il file" "openError": "impossibile aprire il file",
"notificationDenied": "i permessi per le notifiche non sono stati concessi. questa configurazione non ha effetto"
}, },
"filter": { "filter": {
"mostPlayed": "più riprodotti", "mostPlayed": "più riprodotti",
@@ -513,7 +525,9 @@
"openBrowserDevtools": "apri devtools browser", "openBrowserDevtools": "apri devtools browser",
"quit": "$t(common.quit)", "quit": "$t(common.quit)",
"goBack": "torna indietro", "goBack": "torna indietro",
"goForward": "vai avanti" "goForward": "vai avanti",
"privateModeOff": "disabilita modalità privata",
"privateModeOn": "abilita modalità privata"
}, },
"contextMenu": { "contextMenu": {
"addToPlaylist": "$t(action.addToPlaylist)", "addToPlaylist": "$t(action.addToPlaylist)",
@@ -537,7 +551,9 @@
"playSimilarSongs": "$t(player.playSimilarSongs)", "playSimilarSongs": "$t(player.playSimilarSongs)",
"playShuffled": "$t(player.shuffle)", "playShuffled": "$t(player.shuffle)",
"shareItem": "condividi elemento", "shareItem": "condividi elemento",
"showDetails": "mostra info" "showDetails": "mostra info",
"goToAlbum": "vai a $t(entity.album_one)",
"goToAlbumArtist": "vai a $t(entity.albumArtist_one)"
}, },
"home": { "home": {
"mostPlayed": "più riprodotti", "mostPlayed": "più riprodotti",
@@ -674,6 +690,11 @@
"success": "link di condivisione copiato negli appunti (o clicca qui per aprirlo)", "success": "link di condivisione copiato negli appunti (o clicca qui per aprirlo)",
"expireInvalid": "la scadenza deve essere nel futuro", "expireInvalid": "la scadenza deve essere nel futuro",
"createFailed": "condivisione fallita (è abilitata la condivisione?)" "createFailed": "condivisione fallita (è abilitata la condivisione?)"
},
"privateMode": {
"enabled": "la modalità privata è abilitata: lo stato di riproduzione viene ora nascosto alle integrazioni esterne",
"disabled": "la modalità privata è disabilitata: lo stato di riproduzione è ora visibile alle integrazioni esterne abilitate",
"title": "modalità privata"
} }
}, },
"table": { "table": {
+239 -2
View File
@@ -93,7 +93,7 @@
"path": "cesta", "path": "cesta",
"playerMustBePaused": "prehrávač musí byť pozastavený", "playerMustBePaused": "prehrávač musí byť pozastavený",
"preview": "náhľad", "preview": "náhľad",
"previousSong": "predchádzajúci $t(entity.track_one)", "previousSong": "predchádzajúca $t(entity.track_one)",
"quit": "ukončiť", "quit": "ukončiť",
"random": "náhodne", "random": "náhodne",
"rating": "hodnotenie", "rating": "hodnotenie",
@@ -589,6 +589,243 @@
"globalMediaHotkeys_description": "povoliť alebo zakázať použitie vašich klávesových skratiek médií na ovládanie prehrávania", "globalMediaHotkeys_description": "povoliť alebo zakázať použitie vašich klávesových skratiek médií na ovládanie prehrávania",
"homeConfiguration": "konfigurácia domovskej stránky", "homeConfiguration": "konfigurácia domovskej stránky",
"homeConfiguration_description": "konfigurovať, aké položky sú zobrazené a v akom poradí na domovskej stránke", "homeConfiguration_description": "konfigurovať, aké položky sú zobrazené a v akom poradí na domovskej stránke",
"homeFeature": "carousel odporúčania na domovskej stránke" "homeFeature": "carousel odporúčania na domovskej stránke",
"homeFeature_description": "povoľuje zobrazenie veľkoformátového odporúčaného carouselu na domovskej stránke",
"hotkey_browserBack": "naspäť v prehliadači",
"hotkey_browserForward": "dopredu v prehliadači",
"hotkey_favoriteCurrentSong": "obľúbené $t(common.currentSong)",
"hotkey_favoritePreviousSong": "obľúbené $t(common.previousSong)",
"hotkey_globalSearch": "globálne vyhľadávanie",
"hotkey_localSearch": "vyhľadávanie na stránke",
"hotkey_navigateHome": "navigovať domov",
"hotkey_playbackNext": "nasledujúca skladba",
"hotkey_playbackPause": "pozastaviť",
"hotkey_playbackPlay": "prehrať",
"hotkey_playbackPlayPause": "hrať / pozastaviť",
"hotkey_playbackPrevious": "predchádzajúca skladba",
"hotkey_playbackStop": "zastaviť",
"hotkey_rate0": "bez hodnotenia",
"hotkey_rate1": "hodnotené 1 hviezdou",
"hotkey_rate2": "hodnotené 2 hviezdami",
"hotkey_rate3": "hodnotené 3 hviezdami",
"hotkey_rate4": "hodotené 4 hviezdami",
"hotkey_rate5": "hodnotené 5 hviezdami",
"hotkey_skipBackward": "preskočiť dozadu",
"hotkey_skipForward": "preskočiť dopredu",
"hotkey_toggleCurrentSongFavorite": "prepnúť $t(common.currentSong) obľúbené",
"hotkey_toggleFullScreenPlayer": "prepnúť prehrávač na celú obrazovku",
"hotkey_togglePreviousSongFavorite": "prepnúť $t(common.previousSong) obľúbené",
"hotkey_toggleQueue": "prepnúť frontu",
"hotkey_toggleRepeat": "prepnúť opakovanie",
"hotkey_toggleShuffle": "prepnúť náhodné prehrávanie",
"hotkey_unfavoriteCurrentSong": "odobrať z obľúbených $t(common.currentSong)",
"hotkey_unfavoritePreviousSong": "odobrať z obľúbených $t(common.previousSong)",
"hotkey_volumeDown": "znížiť hlasitosť",
"hotkey_volumeMute": "stíšiť hlasitosť",
"hotkey_volumeUp": "zvýšiť hlasitosť",
"hotkey_zoomIn": "priblížiť",
"hotkey_zoomOut": "vzdialiť",
"imageAspectRatio": "použiť pôvodný pomer strán obalu albumu",
"language": "jazyk",
"language_description": "nastaví jazyk aplikácie ($t(common.restartRequired))",
"lastfm": "zobraziť last.fm odkazy",
"lastfm_description": "zobraziť last.fm odkazy na stránky interpreta/albumu",
"lastfmApiKey": "{{lastfm}} API kľúč",
"lastfmApiKey_description": "API kľúč pre {{lastfm}}. vyžaduje sa obálky albumov",
"lyricFetch": "stiahnuť texty skladieb z internetu",
"lyricFetch_description": "stiahnuť texty skladieb z rôznych internetových zdrojov",
"lyricFetchProvider": "poskytovatelia pre sťahovanie textov skladieb",
"lyricFetchProvider_description": "vybrať poskytovateľov pre sťahovanie textov skladieb. poradie poskytovateľov určuje poradie, v ktorom sa budú používať",
"lyricOffset": "posunutie textu skladieb (ms)",
"lyricOffset_description": "posunutie textu voči skladbe vyjadrené v milisekundách",
"notify": "povoliť notifikácie o skladbách",
"notify_description": "zobraziť notifikácie pri zmene aktuálnej skladby",
"minimizeToTray": "minimalizovať do lišty",
"minimizeToTray_description": "minimalizovať aplikáciu do systémovej lišty",
"minimumScrobblePercentage": "minimálna dĺžka pre skroblovanie (percentá)",
"minimumScrobblePercentage_description": "minimálna časť skladby v percentách, ktorá musí byť prehraná pred tým, než je skroblovaná",
"minimumScrobbleSeconds": "minimálna dĺžka skroblovania (sekundy)",
"minimumScrobbleSeconds_description": "minimálna dĺžka časti skladby, ktorá musí byť prehraná pred tým, než je skladba skroblovaná",
"mpvExecutablePath": "cesta k spustiteľnému súboru mpv",
"mpvExecutablePath_description": "nastavuje cestu k spustiteľnému súboru mpv. ak je prázdna, použije sa predvolená cesta",
"mpvExtraParameters": "parametre mpv",
"mpvExtraParameters_help": "jeden na riadok",
"musicbrainz": "zobraziť linky na musicbrainz",
"musicbrainz_description": "zobrazí linky na stránky interpreta/albumu na musicbrainz, ak je vyplnené mbid",
"neteaseTranslation": "Povoliť NetEasy preklady",
"neteaseTranslation_description": "Ak sú povolené, aplikácia stiahne a zobrazí preložené texty skladieb z NetEasy, ak sú dostupné.",
"passwordStore": "ukladanie hesiel/utajených údajov",
"passwordStore_description": "aký spôsob ukladania hesiel/utajených údajov použiť. ak máte problém s ukladaním hesiel, skúste zmeniť nastavenie.",
"playbackStyle": "štýl prehrávania",
"playbackStyle_description": "vyberte štýl prehrávania pre prehrávač skladieb",
"playbackStyle_optionCrossFade": "crossfade",
"playbackStyle_optionNormal": "normálny",
"playButtonBehavior": "správanie sa tlačidla prehrávania",
"playButtonBehavior_description": "nastaví predvolené správanie sa tlačidla prehrávania pri pridávaní skladieb do fronty",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"playButtonBehavior_optionPlay": "$t(player.play)",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playerAlbumArtResolution": "rozlíšenie obrázka albumu",
"playerAlbumArtResolution_description": "rozlíšenie zobrazenia náhľadu veľkých obrázkov albumov. pri väčšom rozlíšení budú krajšie, ale môže sa spomaliť ich načítavanie. predvolené je 0, čo znamená automatické",
"playerbarOpenDrawer": "zobrazenie na celú obrazovku panelom prehrávača",
"playerbarOpenDrawer_description": "umožní kliknutím na panel prehrávača prepnúť zobrazenie prehrávača na celú obrazovku",
"remotePassword": "heslo servera vzdialeného ovládania",
"remotePassword_description": "nastaví heslo pre server diaľkového ovládania. Jeho obsah je odosielaný bez zabezpečenia, preto by ste si mali zvoliť jedinečné heslo, ktoré pre vás nie je dôležité",
"remotePort": "port servera diaľkového ovládania",
"remotePort_description": "nastaví port servera diaľkového ovládania",
"remoteUsername": "používateľské meno servera diaľkového ovládania",
"remoteUsername_description": "nasstaví používateľské meno servera diaľkového ovládania. v prípade, ak sú používateľské meno aj heslo prázdne, je overovanie pri prihlásení vypnuté",
"replayGainClipping": "clipping {{ReplayGain}}",
"replayGainClipping_description": "Zabraňuje clipping-u spôsobenému {{ReplayGain}} automatickým znížením zosilenia",
"replayGainFallback": "fallback {{ReplayGain}}",
"replayGainFallback_description": "zosilenie v db, ktoré sa aplikuje, ak súbor nemá {{ReplayGain}} štítky",
"replayGainMode": "{{ReplayGain}} režim",
"replayGainMode_description": "pozmení zosilenie hlasitosti podľa hodnôt {{ReplayGain}} uložených v metadátach súboru",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"replayGainPreamp": "predzosilenie {{ReplayGain}} dB",
"replayGainPreamp_description": "pozmení predzosilenie použité na hodnoty {{ReplayGain}}",
"sampleRate": "vzorkovacia frekvencia",
"sidePlayQueueStyle_optionAttached": "pripojené",
"sidePlayQueueStyle_optionDetached": "odpojené",
"skipDuration": "dĺžka preskočenia",
"skipDuration_description": "určuje časovú dĺžku posunu pri stlačení tlačidla preskočiť na lište prehrávača",
"skipPlaylistPage": "preskočiť stránku playlistu",
"skipPlaylistPage_description": "pri navigácii v playliste, idete na výber stránky playlistu namiesto predvolenej stránky",
"startMinimized": "spistiť mnimalizované",
"startMinimized_description": "spustí aplikáciu minimalizovanú do systémovej lišty",
"preventSleepOnPlayback": "zabrániť spánku pri prehrávaní",
"preventSleepOnPlayback_description": "pri prehávaní hudby zabráni obrazovke v prechode do spánku",
"theme": "téma",
"theme_description": "nastaví tému aplikácie",
"themeDark": "téma (tmavá)",
"themeDark_description": "nastaví tmavú tému aplikácie",
"themeLight": "téma (svetlá)",
"themeLight_description": "nastaví svetlú tému aplikácie",
"transcodeNote": "zmena sa prejaví po 1 (web) - 2 (mpv) skladbách",
"transcode": "povoliť prekódovanie",
"transcode_description": "umožňuje prekódovanie do rôznych formátov",
"transcodeBitrate": "bitová frekvencia prekódovania",
"transcodeBitrate_description": "určuje bitovú frekvenciu, pri ktorej sa použije prekódovanie. 0 znamená ponechať rozhodnutie na server",
"transcodeFormat": "formát prekódovania",
"transcodeFormat_description": "učuje výstupný formát prekódovania. ak chcete ponechať rozhodnutie na server, nechajte políčko prázdne",
"translationApiProvider": "api poskytovateľa prekladu",
"translationApiProvider_description": "api poskytovateľa prekladu",
"translationApiKey": "api kľúč prekladu",
"translationApiKey_description": "api kľúč pre preklad (Podporuje iba koncové body globálnych služieb)",
"translationTargetLanguage": "cieľový jazyk prekladu",
"translationTargetLanguage_description": "cieľový jazyk, do ktorého sa prekladá",
"trayEnabled": "zobraziť lištu",
"trayEnabled_description": "zobraziť/skryť ikonu/ponuku lišty. ak nie je povolené, taktiež vypne minimalizovanie/zavretie do lišty",
"useSystemTheme": "použiť systémovú tému",
"useSystemTheme_description": "prispôsobiť výber svetlej, či tmavej témy aktuálnej systémovej téme",
"volumeWheelStep": "krok zmeny hlasitosti",
"volumeWheelStep_description": "veľkosť zmeny hlasitosti pri otočení kolieskom myši o jeden krok na ovládači hlasitosti",
"volumeWidth": "šírka posuvného ovládača hlasitosti",
"volumeWidth_description": "šírka ovládača hlasitosti",
"webAudio": "používať webový výstup",
"webAudio_description": "bude sa používať webový výstup, čím povolíte pokročilé funkcie ako replaygain. v prípade problémov voľbu vypnite",
"preservePitch": "zachovať výšku",
"preservePitch_description": "pri zmene rýchlosti prehrávania zostane výška zachovaná",
"windowBarStyle": "štýl okna",
"windowBarStyle_description": "vyberte štýl okna",
"zoom": "percento priblíženia",
"zoom_description": "nastaví percento priblíženia pre aplikáciu",
"sampleRate_description": "vyberte výstupnú vzorkovaciu frekvenciu, ktorá sa použije v prípade, ak je vybraná vzorkovacia frekvencia iná ako je u aktuálnej skladby. pri hodnote menšej ako 8000 sa použije predvolená frekvencia",
"savePlayQueue": "uložiť frontu prehrávania",
"savePlayQueue_description": "uloží frontu prehrávania pri ukončení aplikácie a obnoví ju opäť po jej otvorení",
"scrobble": "skroblovať",
"scrobble_description": "scroblovať vaše prehrávanie na medálny server",
"showSkipButton": "zobraziť tlačítka preskočenia",
"showSkipButton_description": "zobrazí alebo skryje tlačítka preskočenia na lište prehrávača",
"showSkipButtons": "zobraziť tlačítka preskočenia",
"showSkipButtons_description": "zobraziť alebo skryť tlačítka preskočenia na lište prehrávača",
"sidebarCollapsedNavigation": "navigácia bočnej lišty (zasunutá)",
"sidebarCollapsedNavigation_description": "zobraziť alebo skryť navigovanie na zasunutej bočnej lište",
"sidebarConfiguration": "nastavenie bočnej lišty",
"sidebarConfiguration_description": "zvoľte položky a ich poradie, v akom sa zabrazia na bočnej lište",
"sidebarPlaylistList": "playlist bočnej lišty",
"sidebarPlaylistList_description": "zobraziť alebo skryť playlist na bočnej lište",
"sidePlayQueueStyle": "štýl bočnej fronty prehrávania",
"sidePlayQueueStyle_description": "nastaví štýl bočnej fronty prehrávania"
},
"table": {
"column": {
"album": "album",
"albumArtist": "interpret albumu",
"albumCount": "$t(entity.album_other)",
"artist": "$t(entity.artist_one)",
"biography": "životopis",
"bitrate": "bitrate",
"bpm": "bpm",
"channels": "$t(common.channel_other)",
"codec": "$t(common.codec)",
"comment": "komentár",
"dateAdded": "dátum pridania",
"discNumber": "disk",
"favorite": "obľúbené",
"genre": "$t(entity.genre_one)",
"lastPlayed": "posledne hraný",
"path": "cesta",
"playCount": "prehratí",
"rating": "hodnotenie",
"releaseDate": "dátum vydania",
"releaseYear": "rok",
"size": "$t(common.size)",
"songCount": "$t(entity.track_other)",
"title": "názov",
"trackNumber": "skladba"
},
"config": {
"general": {
"autoFitColumns": "automatická šírka stĺpcov",
"followCurrentSong": "nasledovať aktuálnu skladbu",
"displayType": "typ zobrazenia",
"gap": "$t(common.gap)",
"itemGap": "medzera položky (px)",
"itemSize": "veľkosť položky (px)",
"size": "$t(common.size)",
"tableColumns": "stĺpce tabuľky"
},
"label": {
"actions": "$t(common.action_other)",
"album": "$t(entity.album_one)",
"albumArtist": "$t(entity.albumArtist_one)",
"artist": "$t(entity.artist_one)",
"biography": "$t(common.biography)",
"bitrate": "$t(common.bitrate)",
"bpm": "$t(common.bpm)",
"channels": "$t(common.channel_other)",
"codec": "$t(common.codec)",
"dateAdded": "dátum pridania",
"discNumber": "číslo disku",
"duration": "$t(common.duration)",
"favorite": "$t(common.favorite)",
"genre": "$t(entity.genre_one)",
"lastPlayed": "posledne prehraté",
"note": "$t(common.note)",
"owner": "$t(common.owner)",
"path": "$t(common.path)",
"playCount": "počet prehraní",
"rating": "$t(common.rating)",
"releaseDate": "dátum vydania",
"rowIndex": "číslo riadku",
"size": "$t(common.size)",
"songCount": "$t(entity.track_other)",
"title": "$t(common.title)",
"titleCombined": "$t(common.title) (kombinovaný)",
"trackNumber": "číslo skladby",
"year": "$t(common.year)"
},
"view": {
"card": "karta",
"grid": "mriežka",
"list": "zoznam",
"poster": "plagát",
"table": "tabuľka"
}
}
} }
} }
+19 -3
View File
@@ -286,6 +286,11 @@
"updateServer": { "updateServer": {
"success": "sunucu başarıyla güncellendi", "success": "sunucu başarıyla güncellendi",
"title": "sunucuyu güncelle" "title": "sunucuyu güncelle"
},
"privateMode": {
"enabled": "gizli mod etkinleştirildi, oynatma durumu artık harici eklentilerden gizlendi",
"disabled": "gizli mod devre dışı bırakıldı, oynatma durumu artık etkinleştirilmiş harici eklentiler tarafından görülebilir",
"title": "gizli mod"
} }
}, },
"page": { "page": {
@@ -322,7 +327,9 @@
"addFavorite": "$t(action.addToFavorites)", "addFavorite": "$t(action.addToFavorites)",
"playShuffled": "$t(player.shuffle)", "playShuffled": "$t(player.shuffle)",
"shareItem": "öğeyi paylaş", "shareItem": "öğeyi paylaş",
"showDetails": "bilgi al" "showDetails": "bilgi al",
"goToAlbum": "$t(entity.album_one) sayfasına git",
"goToAlbumArtist": "$t(entity.albumArtist_one) sayfasına git"
}, },
"manageServers": { "manageServers": {
"url": "URL", "url": "URL",
@@ -436,7 +443,9 @@
"quit": "$t(common.quit)", "quit": "$t(common.quit)",
"selectServer": "sunucu seç", "selectServer": "sunucu seç",
"settings": "$t(common.setting_other)", "settings": "$t(common.setting_other)",
"version": "{{version}} sürümü" "version": "{{version}} sürümü",
"privateModeOff": "gizli modu kapat",
"privateModeOn": "gizli modu aç"
} }
}, },
"player": { "player": {
@@ -713,7 +722,14 @@
"themeLight": "tema (açık)", "themeLight": "tema (açık)",
"themeLight_description": "uygulama için kullanılacak açık temayı ayarlar", "themeLight_description": "uygulama için kullanılacak açık temayı ayarlar",
"transcodeNote": "1 (web) - 2 (mpv) şarkıdan sonra etkili olur", "transcodeNote": "1 (web) - 2 (mpv) şarkıdan sonra etkili olur",
"transcode": "kod dönüştürmeyi etkinleştir" "transcode": "kod dönüştürmeyi etkinleştir",
"discordDisplayType": "{{discord}} varlık gösterge türü",
"discordDisplayType_description": "durumunuzda dinlediğiniz şarkı olarak değiştirir",
"discordDisplayType_songname": "şarkı ismi",
"discordDisplayType_artistname": "Sanatçı adı(ları)",
"hotkey_navigateHome": "ana sayfaya git",
"preventSleepOnPlayback": "oynatma sırasında uykuyu önle",
"preventSleepOnPlayback_description": "müzik çalarken ekranın uyku moduna geçmesini önle"
}, },
"table": { "table": {
"column": { "column": {
+6 -6
View File
@@ -127,13 +127,13 @@
"playlist_other": "播放列表", "playlist_other": "播放列表",
"artist_other": "艺术家", "artist_other": "艺术家",
"folderWithCount_other": "{{count}} 个文件夹", "folderWithCount_other": "{{count}} 个文件夹",
"track_other": "曲", "track_other": "曲",
"favorite_other": "收藏", "favorite_other": "收藏",
"artistWithCount_other": "{{count}} 位艺术家", "artistWithCount_other": "{{count}} 位艺术家",
"folder_other": "文件夹", "folder_other": "文件夹",
"smartPlaylist": "智能$t(entity.playlist_one)", "smartPlaylist": "智能$t(entity.playlist_one)",
"genreWithCount_other": "{{count}} 种流派", "genreWithCount_other": "{{count}} 种流派",
"trackWithCount_other": "{{count}} 首曲", "trackWithCount_other": "{{count}} 首曲",
"play_other": "{{count}} 次播放", "play_other": "{{count}} 次播放",
"song_other": "歌曲" "song_other": "歌曲"
}, },
@@ -167,7 +167,7 @@
"skip_forward": "向前跳过", "skip_forward": "向前跳过",
"playbackSpeed": "播放速度", "playbackSpeed": "播放速度",
"pause": "暂停", "pause": "暂停",
"playSimilarSongs": "播放类似的曲", "playSimilarSongs": "播放类似的曲",
"viewQueue": "查看播放队列" "viewQueue": "查看播放队列"
}, },
"setting": { "setting": {
@@ -205,7 +205,7 @@
"enableRemote_description": "启用远程控制服务器,以允许其他设备控制此应用", "enableRemote_description": "启用远程控制服务器,以允许其他设备控制此应用",
"remotePort_description": "设置远程服务器端口", "remotePort_description": "设置远程服务器端口",
"hotkey_skipBackward": "向后跳过", "hotkey_skipBackward": "向后跳过",
"replayGainMode_description": "根据乐曲元数据中存储的{{ReplayGain}}值调整音量增益", "replayGainMode_description": "根据文件元数据中存储的 {{ReplayGain}} 值调整音量增益",
"volumeWheelStep_description": "在音量滑块上滚动鼠标滚轮时要更改的音量大小", "volumeWheelStep_description": "在音量滑块上滚动鼠标滚轮时要更改的音量大小",
"theme_description": "设置应用的主题", "theme_description": "设置应用的主题",
"hotkey_playbackPause": "暂停", "hotkey_playbackPause": "暂停",
@@ -290,7 +290,7 @@
"playbackStyle_optionNormal": "正常", "playbackStyle_optionNormal": "正常",
"windowBarStyle": "窗口顶栏风格", "windowBarStyle": "窗口顶栏风格",
"floatingQueueArea": "显示浮动队列悬停区域", "floatingQueueArea": "显示浮动队列悬停区域",
"replayGainFallback_description": "乐曲没有{{ReplayGain}}标签应用增益(以分贝为单位)", "replayGainFallback_description": "如果文件没有 {{ReplayGain}} 标签,则在数据库中应用增益",
"hotkey_toggleRepeat": "切换循环", "hotkey_toggleRepeat": "切换循环",
"lyricOffset_description": "将歌词偏移指定的毫秒数", "lyricOffset_description": "将歌词偏移指定的毫秒数",
"sidebarConfiguration_description": "选择侧边栏包含的项目与顺序", "sidebarConfiguration_description": "选择侧边栏包含的项目与顺序",
@@ -464,7 +464,7 @@
"albumArtist": "$t(entity.albumArtist_one)", "albumArtist": "$t(entity.albumArtist_one)",
"releaseYear": "发布年份", "releaseYear": "发布年份",
"biography": "个人简介", "biography": "个人简介",
"songCount": "曲数量", "songCount": "曲数量",
"random": "随机", "random": "随机",
"lastPlayed": "上次播放过", "lastPlayed": "上次播放过",
"toYear": "从年份", "toYear": "从年份",
@@ -0,0 +1,55 @@
import { createSocket } from 'dgram';
import { ipcMain } from 'electron';
import { DiscoveredServerItem, ServerType } from '/@/shared/types/types';
type JellyfinResponse = {
Address: string;
Id: string;
Name: string;
};
function discoverAll(reply: (server: DiscoveredServerItem) => void) {
return Promise.all([discoverJellyfin(reply)]);
}
function discoverJellyfin(reply: (server: DiscoveredServerItem) => void) {
const sock = createSocket('udp4');
sock.on('message', (msg) => {
try {
const response: JellyfinResponse = JSON.parse(msg.toString('utf-8'));
reply({
name: response.Name,
type: ServerType.JELLYFIN,
url: response.Address,
});
} catch (e) {
// Got a spurious response, ignore?
console.error(e);
}
});
sock.bind(() => {
sock.setBroadcast(true);
// Send a broadcast packet to both loopback and default route, allowing discovery of same-machine instances
sock.send('who is JellyfinServer?', 7359, '127.255.255.255');
sock.send('who is JellyfinServer?', 7359, '255.255.255.255');
});
return new Promise<void>((resolve) => {
setTimeout(() => {
sock.close();
resolve();
}, 3000);
});
}
ipcMain.on('autodiscover-ping', (ev) => {
if (ev.ports.length === 0) throw new Error('Expected a port to stream autodiscovery results');
const port = ev.ports[0];
discoverAll((result) => port.postMessage(result))
.then(() => port.close())
.catch((err) => console.error(err));
});
+1
View File
@@ -1,3 +1,4 @@
import './autodiscover';
import './lyrics'; import './lyrics';
import './player'; import './player';
import './remote'; import './remote';
+2 -3
View File
@@ -1,14 +1,13 @@
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { load } from 'cheerio'; import { load } from 'cheerio';
import { orderSearchResults } from './shared';
import { import {
InternetProviderLyricResponse, InternetProviderLyricResponse,
InternetProviderLyricSearchResponse, InternetProviderLyricSearchResponse,
LyricSearchQuery, LyricSearchQuery,
LyricSource, LyricSource,
} from '/@/shared/types/domain/lyric-domain-types'; } from '.';
import { orderSearchResults } from './shared';
const SEARCH_URL = 'https://genius.com/api/search/song'; const SEARCH_URL = 'https://genius.com/api/search/song';
+29 -6
View File
@@ -17,12 +17,35 @@ import {
getSearchResults as searchNetease, getSearchResults as searchNetease,
} from './netease'; } from './netease';
import { import { Song } from '/@/shared/types/domain-types';
InternetProviderLyricResponse,
InternetProviderLyricSearchResponse, export enum LyricSource {
LyricSource, GENIUS = 'Genius',
} from '/@/shared/types/domain/lyric-domain-types'; LRCLIB = 'lrclib.net',
import { Song } from '/@/shared/types/domain/song-domain-types'; NETEASE = 'NetEase',
}
export type FullLyricsMetadata = Omit<InternetProviderLyricResponse, 'id' | 'lyrics' | 'source'> & {
lyrics: LyricsResponse;
remote: boolean;
source: string;
};
export type InternetProviderLyricResponse = {
artist: string;
id: string;
lyrics: string;
name: string;
source: LyricSource;
};
export type InternetProviderLyricSearchResponse = {
artist: string;
id: string;
name: string;
score?: number;
source: LyricSource;
};
export type LyricGetQuery = { export type LyricGetQuery = {
remoteSongId: string; remoteSongId: string;
+2 -3
View File
@@ -1,14 +1,13 @@
// Credits to https://github.com/tranxuanthang/lrcget for API implementation // Credits to https://github.com/tranxuanthang/lrcget for API implementation
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { orderSearchResults } from './shared';
import { import {
InternetProviderLyricResponse, InternetProviderLyricResponse,
InternetProviderLyricSearchResponse, InternetProviderLyricSearchResponse,
LyricSearchQuery, LyricSearchQuery,
LyricSource, LyricSource,
} from '/@/shared/types/domain/lyric-domain-types'; } from '.';
import { orderSearchResults } from './shared';
const FETCH_URL = 'https://lrclib.net/api/get'; const FETCH_URL = 'https://lrclib.net/api/get';
const SEEARCH_URL = 'https://lrclib.net/api/search'; const SEEARCH_URL = 'https://lrclib.net/api/search';
+27 -28
View File
@@ -1,20 +1,43 @@
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { store } from '../settings';
import { orderSearchResults } from './shared';
import { import {
InternetProviderLyricResponse, InternetProviderLyricResponse,
InternetProviderLyricSearchResponse, InternetProviderLyricSearchResponse,
LyricSearchQuery, LyricSearchQuery,
LyricSource, LyricSource,
} from '/@/shared/types/domain/lyric-domain-types'; } from '.';
import { store } from '../settings';
import { orderSearchResults } from './shared';
const SEARCH_URL = 'https://music.163.com/api/search/get'; const SEARCH_URL = 'https://music.163.com/api/search/get';
const LYRICS_URL = 'https://music.163.com/api/song/lyric'; const LYRICS_URL = 'https://music.163.com/api/song/lyric';
// Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/netease.ts // Adapted from https://github.com/NyaomiDEV/Sunamu/blob/master/src/main/lyricproviders/netease.ts
export interface Result {
hasMore: boolean;
songCount: number;
songs: Song[];
}
export interface Song {
album: Album;
alias: string[];
artists: Artist[];
copyrightId: number;
duration: number;
fee: number;
ftype: number;
id: number;
mark: number;
mvid: number;
name: string;
rtype: number;
rUrl: null;
status: number;
transNames?: string[];
}
interface Album { interface Album {
artist: Artist; artist: Artist;
copyrightId: number; copyrightId: number;
@@ -46,30 +69,6 @@ interface NetEaseResponse {
result: Result; result: Result;
} }
interface Result {
hasMore: boolean;
songCount: number;
songs: Song[];
}
interface Song {
album: Album;
alias: string[];
artists: Artist[];
copyrightId: number;
duration: number;
fee: number;
ftype: number;
id: number;
mark: number;
mvid: number;
name: string;
rtype: number;
rUrl: null;
status: number;
transNames?: string[];
}
export async function getLyricsBySongId(songId: string): Promise<null | string> { export async function getLyricsBySongId(songId: string): Promise<null | string> {
let result: AxiosResponse<any, any>; let result: AxiosResponse<any, any>;
try { try {
+4 -2
View File
@@ -1,7 +1,9 @@
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { InternetProviderLyricSearchResponse } from '/@/shared/types/domain/lyric-domain-types'; import {
import { LyricSearchQuery } from '/@/shared/types/domain/lyric-domain-types'; InternetProviderLyricSearchResponse,
LyricSearchQuery,
} from '/@/shared/types/domain-types';
export const orderSearchResults = (args: { export const orderSearchResults = (args: {
params: LyricSearchQuery; params: LyricSearchQuery;
+2 -2
View File
@@ -11,8 +11,8 @@ import manifest from './manifest.json';
import { getMainWindow } from '/@/main/index'; import { getMainWindow } from '/@/main/index';
import { isLinux } from '/@/main/utils'; import { isLinux } from '/@/main/utils';
import { QueueSong } from '/@/shared/types/domain/player-domain-types'; import { QueueSong } from '/@/shared/types/domain-types';
import { ClientEvent, ServerEvent } from '/@/shared/types/domain/remote-types'; import { ClientEvent, ServerEvent } from '/@/shared/types/remote-types';
import { PlayerRepeat, PlayerStatus, SongState } from '/@/shared/types/types'; import { PlayerRepeat, PlayerStatus, SongState } from '/@/shared/types/types';
let mprisPlayer: any | undefined; let mprisPlayer: any | undefined;
+6 -2
View File
@@ -2,7 +2,7 @@ import { ipcMain } from 'electron';
import Player from 'mpris-service'; import Player from 'mpris-service';
import { getMainWindow } from '/@/main/index'; import { getMainWindow } from '/@/main/index';
import { QueueSong } from '/@/shared/types/domain/player-domain-types'; import { QueueSong } from '/@/shared/types/domain-types';
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types'; import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
const mprisPlayer = Player({ const mprisPlayer = Player({
@@ -107,6 +107,10 @@ mprisPlayer.on('seek', (event: number) => {
}); });
}); });
mprisPlayer.on('raise', () => {
getMainWindow()?.show();
});
ipcMain.on('update-position', (_event, arg: number) => { ipcMain.on('update-position', (_event, arg: number) => {
mprisPlayer.getPosition = () => arg * 1e6; mprisPlayer.getPosition = () => arg * 1e6;
}); });
@@ -168,7 +172,7 @@ ipcMain.on('update-song', (_event, song: QueueSong | undefined) => {
'xesam:contentCreated': song.releaseDate, 'xesam:contentCreated': song.releaseDate,
'xesam:discNumber': song.discNumber ? song.discNumber : null, 'xesam:discNumber': song.discNumber ? song.discNumber : null,
'xesam:genre': song.genres?.length ? song.genres.map((genre: any) => genre.name) : null, 'xesam:genre': song.genres?.length ? song.genres.map((genre: any) => genre.name) : null,
'xesam:lastUsed': song.userLastPlayedDate, 'xesam:lastUsed': song.lastPlayedAt,
'xesam:title': song.name || null, 'xesam:title': song.name || null,
'xesam:trackNumber': song.trackNumber ? song.trackNumber : null, 'xesam:trackNumber': song.trackNumber ? song.trackNumber : null,
'xesam:useCount': 'xesam:useCount':
+7
View File
@@ -44,6 +44,10 @@ export default class AppUpdater {
log.transports.file.level = 'info'; log.transports.file.level = 'info';
autoUpdater.logger = autoUpdaterLogInterface; autoUpdater.logger = autoUpdaterLogInterface;
autoUpdater.checkForUpdatesAndNotify(); autoUpdater.checkForUpdatesAndNotify();
if (store.get('release_channel') === 'beta') {
autoUpdater.channel = 'beta';
}
} }
} }
@@ -489,7 +493,10 @@ async function createWindow(first = true): Promise<void> {
const menuBuilder = new MenuBuilder(mainWindow); const menuBuilder = new MenuBuilder(mainWindow);
menuBuilder.buildMenu(); menuBuilder.buildMenu();
if (process.platform !== 'darwin') {
Menu.setApplicationMenu(null); Menu.setApplicationMenu(null);
}
// Open URLs in the user's browser // Open URLs in the user's browser
mainWindow.webContents.setWindowOpenHandler((edata) => { mainWindow.webContents.setWindowOpenHandler((edata) => {
+23
View File
@@ -0,0 +1,23 @@
import { ipcRenderer } from 'electron';
import { DiscoveredServerItem } from '../shared/types/types';
const discover = (onReply: (server: DiscoveredServerItem) => void): Promise<void> => {
const { port1: local, port2: remote } = new MessageChannel();
ipcRenderer.postMessage('autodiscover-ping', {}, [remote]);
local.onmessage = (ev) => {
onReply(ev.data);
};
return new Promise<void>((resolve) => {
local.addEventListener('close', () => resolve());
});
};
export const autodiscover = {
discover,
};
export type AutoDiscover = typeof autodiscover;
+2
View File
@@ -1,6 +1,7 @@
import { electronAPI } from '@electron-toolkit/preload'; import { electronAPI } from '@electron-toolkit/preload';
import { contextBridge } from 'electron'; import { contextBridge } from 'electron';
import { autodiscover } from './autodiscover';
import { browser } from './browser'; import { browser } from './browser';
import { discordRpc } from './discord-rpc'; import { discordRpc } from './discord-rpc';
import { ipc } from './ipc'; import { ipc } from './ipc';
@@ -13,6 +14,7 @@ import { utils } from './utils';
// Custom APIs for renderer // Custom APIs for renderer
const api = { const api = {
autodiscover,
browser, browser,
discordRpc, discordRpc,
ipc, ipc,
+1 -1
View File
@@ -7,7 +7,7 @@ import {
LyricSource, LyricSource,
} from '../main/features/core/lyrics'; } from '../main/features/core/lyrics';
import { QueueSong } from '/@/shared/types/domain/player-domain-types'; import { QueueSong } from '/@/shared/types/domain-types';
const getRemoteLyricsBySong = (song: QueueSong) => { const getRemoteLyricsBySong = (song: QueueSong) => {
const result = ipcRenderer.invoke('lyric-by-song', song); const result = ipcRenderer.invoke('lyric-by-song', song);
+1 -1
View File
@@ -1,6 +1,6 @@
import { ipcRenderer, IpcRendererEvent } from 'electron'; import { ipcRenderer, IpcRendererEvent } from 'electron';
import { PlayerData } from '/@/shared/types/domain/player-domain-types'; import { PlayerData } from '/@/shared/types/domain-types';
const initialize = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => { const initialize = (data: { extraParameters?: string[]; properties?: Record<string, any> }) => {
return ipcRenderer.invoke('player-initialize', data); return ipcRenderer.invoke('player-initialize', data);
+1 -1
View File
@@ -1,6 +1,6 @@
import { ipcRenderer, IpcRendererEvent } from 'electron'; import { ipcRenderer, IpcRendererEvent } from 'electron';
import { QueueSong, QueueSong } from '/@/shared/types/domain/player-domain-types'; import { QueueSong } from '/@/shared/types/domain-types';
import { PlayerStatus } from '/@/shared/types/types'; import { PlayerStatus } from '/@/shared/types/types';
const requestFavorite = ( const requestFavorite = (
-172
View File
@@ -1,172 +0,0 @@
import {
ApiController,
ApiControllerError,
ApiControllerFn,
} from '/@/shared/types/adapter/api-controller-types';
import { ServerListItem } from '/@/shared/types/domain/server-domain-types';
import { logger } from '/@/shared/utils/logger';
export interface LoggingOptions {
logErrors?: boolean;
logPerformance?: boolean;
logRequests?: boolean;
logResponses?: boolean;
maxRequestSize?: number;
maxResponseSize?: number;
}
type LoggedApiControllerFn<TRequest, TResponse> = (
request: TRequest,
server: ServerListItem,
options?: any,
) => Promise<[ApiControllerError, null] | [null, TResponse]>;
export function createLoggedApiController(
controller: ApiController,
options: LoggingOptions = {},
): ApiController {
const loggedController: any = {};
// Log utility functions
loggedController._utility = createLoggedUtility(controller._utility);
// Log all controller methods
for (const [sectionKey, section] of Object.entries(controller)) {
if (sectionKey === '_utility') continue;
loggedController[sectionKey] = {};
for (const [methodKey, method] of Object.entries(section as Record<string, any>)) {
if (typeof method === 'function') {
const functionName = `${sectionKey}.${methodKey}`;
if (methodKey === 'authenticate' || methodKey === 'getType') {
// Special handling for non-standard API functions
loggedController[sectionKey][methodKey] = (...args: any[]) => {
logger.info(`[API] ${functionName} called`, {
args: JSON.stringify(args, null, 2),
});
return method(...args);
};
} else {
loggedController[sectionKey][methodKey] = createLoggedFunction(
method as ApiControllerFn<any, any>,
functionName,
options,
);
}
} else {
loggedController[sectionKey][methodKey] = method;
}
}
}
return loggedController as ApiController;
}
function createLoggedFunction<TRequest, TResponse>(
originalFn: ApiControllerFn<TRequest, TResponse> | undefined,
functionName: string,
options: LoggingOptions = {},
): LoggedApiControllerFn<TRequest, TResponse> | undefined {
if (!originalFn) {
return undefined;
}
return async (request: TRequest, requestOptions?: any) => {
const startTime = Date.now();
const requestId = Math.random().toString(36).substring(2, 15);
const {
logErrors = true,
logPerformance = true,
logRequests = true,
logResponses = true,
maxRequestSize = 1000,
maxResponseSize = 1000,
} = options;
if (logRequests) {
const requestStr = JSON.stringify(request, null, 2);
const truncatedRequest =
requestStr.length > maxRequestSize
? requestStr.substring(0, maxRequestSize) + '...'
: requestStr;
logger.info(`[API] ${functionName} called`, {
request: truncatedRequest,
requestId,
});
}
try {
const result = await originalFn(request, requestOptions);
const duration = Date.now() - startTime;
if (result[0]) {
// Error response
if (logErrors) {
const error = result[0] as ApiControllerError;
logger.error(`[API] ${functionName} failed`, {
duration: logPerformance ? `${duration}ms` : undefined,
error: {
code: error.code,
message: error.message,
},
requestId,
});
}
} else {
// Success response
if (logResponses) {
const response = result[1];
const responseStr = JSON.stringify(response);
const truncatedResponse =
responseStr.length > maxResponseSize
? responseStr.substring(0, maxResponseSize) + '...'
: responseStr;
logger.info(`[API] ${functionName} succeeded`, {
duration: logPerformance ? `${duration}ms` : undefined,
requestId,
response: truncatedResponse,
responseSize: responseStr.length,
responseType: typeof response,
});
}
}
return result;
} catch (error) {
const duration = Date.now() - startTime;
if (logErrors) {
logger.error(`[API] ${functionName} threw exception`, {
duration: logPerformance ? `${duration}ms` : undefined,
error: error instanceof Error ? error.message : String(error),
requestId,
stack: error instanceof Error ? error.stack : undefined,
});
}
throw error;
}
};
}
function createLoggedUtility<T extends Record<string, any>>(utility: T): T {
const loggedUtility: any = {};
for (const [key, value] of Object.entries(utility)) {
if (typeof value === 'function') {
loggedUtility[key] = (...args: any[]) => {
logger.debug(`[API] _utility.${key} called`, {
args: JSON.stringify(args, null, 2),
});
return value(...args);
};
} else {
loggedUtility[key] = value;
}
}
return loggedUtility as T;
}
-75
View File
@@ -1,75 +0,0 @@
import { createLoggedApiController } from '/@/renderer/api/api-controller-logger';
import { useAuthStore } from '/@/renderer/store';
import {
createApiClient as subsonicApiClient,
authenticate as subsonicAuthenticate,
controller as subsonicController,
middleware as subsonicMiddleware,
} from '/@/shared/api/subsonic/subsonic-controller';
import { ApiController } from '/@/shared/types/adapter/api-controller-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
export const serverApiMap = {
[ServerType.JELLYFIN]: {
apiClient: null,
authenticate: null,
controller: {},
middleware: null,
},
[ServerType.NAVIDROME]: {
apiClient: null,
authenticate: null,
controller: {},
middleware: null,
},
[ServerType.SUBSONIC]: {
apiClient: subsonicApiClient,
authenticate: subsonicAuthenticate,
controller: subsonicController,
middleware: subsonicMiddleware,
},
};
const getApiByServer = (serverId: string): ApiController => {
const servers = useAuthStore.getState().serverList;
const server = servers[serverId];
if (!server) {
throw new Error('No server or api client selected');
}
const { apiClient, controller, middleware } = serverApiMap[server.type];
if (!apiClient) {
throw new Error('No api client found');
}
const client = apiClient(server, middleware);
return createLoggedApiController(controller(client, server));
};
const getAppApi = () => {
const servers = useAuthStore.getState().serverList;
return Object.entries(servers).reduce(
(acc, [id]) => {
acc[id] = getApiByServer(id);
return acc;
},
{} as Record<string, ApiController>,
);
};
export const api = {
authenticate: (serverType: ServerType) => {
const { authenticate } = serverApiMap[serverType];
if (!serverType || !authenticate) {
throw new Error();
}
return authenticate;
},
controller: getAppApi(),
};
+5 -3
View File
@@ -4,9 +4,11 @@ import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-control
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller'; import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
import { useAuthStore } from '/@/renderer/store'; import { useAuthStore } from '/@/renderer/store';
import { toast } from '/@/shared/components/toast/toast'; import { toast } from '/@/shared/components/toast/toast';
import { ControllerEndpoint } from '/@/shared/types/domain/api-domain-types'; import {
import { AuthenticationResponse } from '/@/shared/types/domain/auth-domain-types'; AuthenticationResponse,
import { ServerType } from '/@/shared/types/domain/server-domain-types'; ControllerEndpoint,
ServerType,
} from '/@/shared/types/domain-types';
type ApiController = { type ApiController = {
jellyfin: ControllerEndpoint; jellyfin: ControllerEndpoint;
+1 -1
View File
@@ -11,7 +11,7 @@ import { authenticationFailure } from '/@/renderer/api/utils';
import { useAuthStore } from '/@/renderer/store'; import { useAuthStore } from '/@/renderer/store';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types'; import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { getClientType } from '/@/shared/api/utils'; import { getClientType } from '/@/shared/api/utils';
import { ServerListItem } from '/@/shared/types/domain/server-domain-types'; import { ServerListItem } from '/@/shared/types/domain-types';
const c = initContract(); const c = initContract();
@@ -6,13 +6,19 @@ import { JFSongListSort, JFSortOrder } from '/@/shared/api/jellyfin.types';
import { jfNormalize } from '/@/shared/api/jellyfin/jellyfin-normalize'; import { jfNormalize } from '/@/shared/api/jellyfin/jellyfin-normalize';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types'; import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { getFeatures, hasFeature, VersionInfo } from '/@/shared/api/utils'; import { getFeatures, hasFeature, VersionInfo } from '/@/shared/api/utils';
import { albumListSortMap } from '/@/shared/types/domain/album-domain-types'; import {
import { ControllerEndpoint } from '/@/shared/types/domain/api-domain-types'; albumArtistListSortMap,
import { albumArtistListSortMap } from '/@/shared/types/domain/artist-domain-types'; albumListSortMap,
import { Played } from '/@/shared/types/domain/player-domain-types'; ControllerEndpoint,
import { ServerFeature } from '/@/shared/types/domain/server-domain-types'; genreListSortMap,
import { LibraryItem, sortOrderMap } from '/@/shared/types/domain/shared-domain-types'; LibraryItem,
import { Song, songListSortMap } from '/@/shared/types/domain/song-domain-types'; Played,
playlistListSortMap,
Song,
songListSortMap,
sortOrderMap,
} from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
const formatCommaDelimitedString = (value: string[]) => { const formatCommaDelimitedString = (value: string[]) => {
return value.join(','); return value.join(',');
@@ -320,7 +326,7 @@ export const JellyfinController: ControllerEndpoint = {
SearchTerm: query.searchTerm, SearchTerm: query.searchTerm,
SortBy: albumListSortMap.jellyfin[query.sortBy] || 'SortName', SortBy: albumListSortMap.jellyfin[query.sortBy] || 'SortName',
SortOrder: sortOrderMap.jellyfin[query.sortOrder], SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.offset, StartIndex: query.startIndex,
...query._custom?.jellyfin, ...query._custom?.jellyfin,
Years: yearsFilter, Years: yearsFilter,
}, },
@@ -332,14 +338,14 @@ export const JellyfinController: ControllerEndpoint = {
return { return {
items: res.body.Items.map((item) => jfNormalize.album(item, apiClientProps.server)), items: res.body.Items.map((item) => jfNormalize.album(item, apiClientProps.server)),
offset: query.offset, startIndex: query.startIndex,
totalRecordCount: res.body.TotalRecordCount, totalRecordCount: res.body.TotalRecordCount,
}; };
}, },
getAlbumListCount: async ({ apiClientProps, query }) => getAlbumListCount: async ({ apiClientProps, query }) =>
JellyfinController.getAlbumList({ JellyfinController.getAlbumList({
apiClientProps, apiClientProps,
query: { ...query, limit: 1, offset: 0 }, query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!), }).then((result) => result!.totalRecordCount!),
getArtistList: async (args) => { getArtistList: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -354,7 +360,7 @@ export const JellyfinController: ControllerEndpoint = {
SearchTerm: query.searchTerm, SearchTerm: query.searchTerm,
SortBy: albumArtistListSortMap.jellyfin[query.sortBy] || 'SortName,Name', SortBy: albumArtistListSortMap.jellyfin[query.sortBy] || 'SortName,Name',
SortOrder: sortOrderMap.jellyfin[query.sortOrder], SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.offset, StartIndex: query.startIndex,
UserId: apiClientProps.server?.userId || undefined, UserId: apiClientProps.server?.userId || undefined,
}, },
}); });
@@ -367,14 +373,14 @@ export const JellyfinController: ControllerEndpoint = {
items: res.body.Items.map((item) => items: res.body.Items.map((item) =>
jfNormalize.albumArtist(item, apiClientProps.server), jfNormalize.albumArtist(item, apiClientProps.server),
), ),
offset: query.offset, startIndex: query.startIndex,
totalRecordCount: res.body.TotalRecordCount, totalRecordCount: res.body.TotalRecordCount,
}; };
}, },
getArtistListCount: async ({ apiClientProps, query }) => getArtistListCount: async ({ apiClientProps, query }) =>
JellyfinController.getArtistList({ JellyfinController.getArtistList({
apiClientProps, apiClientProps,
query: { ...query, limit: 1, offset: 0 }, query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!), }).then((result) => result!.totalRecordCount!),
getDownloadUrl: (args) => { getDownloadUrl: (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -396,7 +402,7 @@ export const JellyfinController: ControllerEndpoint = {
SearchTerm: query?.searchTerm, SearchTerm: query?.searchTerm,
SortBy: genreListSortMap.jellyfin[query.sortBy] || 'SortName', SortBy: genreListSortMap.jellyfin[query.sortBy] || 'SortName',
SortOrder: sortOrderMap.jellyfin[query.sortOrder], SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.offset, StartIndex: query.startIndex,
UserId: apiClientProps.server?.userId, UserId: apiClientProps.server?.userId,
}, },
}); });
@@ -407,7 +413,7 @@ export const JellyfinController: ControllerEndpoint = {
return { return {
items: res.body.Items.map((item) => jfNormalize.genre(item, apiClientProps.server)), items: res.body.Items.map((item) => jfNormalize.genre(item, apiClientProps.server)),
offset: query.offset || 0, startIndex: query.startIndex || 0,
totalRecordCount: res.body?.TotalRecordCount || 0, totalRecordCount: res.body?.TotalRecordCount || 0,
}; };
}, },
@@ -456,7 +462,7 @@ export const JellyfinController: ControllerEndpoint = {
return { return {
items: musicFolders.map(jfNormalize.musicFolder), items: musicFolders.map(jfNormalize.musicFolder),
offset: 0, startIndex: 0,
totalRecordCount: musicFolders?.length || 0, totalRecordCount: musicFolders?.length || 0,
}; };
}, },
@@ -503,7 +509,7 @@ export const JellyfinController: ControllerEndpoint = {
SearchTerm: query.searchTerm, SearchTerm: query.searchTerm,
SortBy: playlistListSortMap.jellyfin[query.sortBy], SortBy: playlistListSortMap.jellyfin[query.sortBy],
SortOrder: sortOrderMap.jellyfin[query.sortOrder], SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.offset, StartIndex: query.startIndex,
}, },
}); });
@@ -513,14 +519,14 @@ export const JellyfinController: ControllerEndpoint = {
return { return {
items: res.body.Items.map((item) => jfNormalize.playlist(item, apiClientProps.server)), items: res.body.Items.map((item) => jfNormalize.playlist(item, apiClientProps.server)),
offset: 0, startIndex: 0,
totalRecordCount: res.body.TotalRecordCount, totalRecordCount: res.body.TotalRecordCount,
}; };
}, },
getPlaylistListCount: async ({ apiClientProps, query }) => getPlaylistListCount: async ({ apiClientProps, query }) =>
JellyfinController.getPlaylistList({ JellyfinController.getPlaylistList({
apiClientProps, apiClientProps,
query: { ...query, limit: 1, offset: 0 }, query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!), }).then((result) => result!.totalRecordCount!),
getPlaylistSongList: async (args) => { getPlaylistSongList: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -536,10 +542,6 @@ export const JellyfinController: ControllerEndpoint = {
query: { query: {
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags', Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags',
IncludeItemTypes: 'Audio', IncludeItemTypes: 'Audio',
Limit: query.limit,
SortBy: query.sortBy ? songListSortMap.jellyfin[query.sortBy] : undefined,
SortOrder: query.sortOrder ? sortOrderMap.jellyfin[query.sortOrder] : undefined,
StartIndex: query.startIndex,
UserId: apiClientProps.server?.userId, UserId: apiClientProps.server?.userId,
}, },
}); });
@@ -550,7 +552,7 @@ export const JellyfinController: ControllerEndpoint = {
return { return {
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')), items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
offset: query.startIndex, startIndex: 0,
totalRecordCount: res.body.TotalRecordCount, totalRecordCount: res.body.TotalRecordCount,
}; };
}, },
@@ -600,7 +602,7 @@ export const JellyfinController: ControllerEndpoint = {
return { return {
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')), items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
offset: 0, startIndex: 0,
totalRecordCount: res.body.Items.length || 0, totalRecordCount: res.body.Items.length || 0,
}; };
}, },
@@ -625,8 +627,11 @@ export const JellyfinController: ControllerEndpoint = {
getSimilarSongs: async (args) => { getSimilarSongs: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
// Prefer getSimilarSongs, where possible. Fallback to InstantMix if (apiClientProps.server?.preferInstantMix !== true) {
// where no similar songs were found. // Prefer getSimilarSongs, where possible, and not overridden.
// InstantMix can be overridden by plugins, so this may be preferred by the user.
// Otherwise, similarSongs may have a better output than InstantMix, if sufficient
// data exists from the server.
const res = await jfApiClient(apiClientProps).getSimilarSongs({ const res = await jfApiClient(apiClientProps).getSimilarSongs({
params: { params: {
itemId: query.songId, itemId: query.songId,
@@ -651,6 +656,7 @@ export const JellyfinController: ControllerEndpoint = {
return results; return results;
} }
} }
}
const mix = await jfApiClient(apiClientProps).getInstantMix({ const mix = await jfApiClient(apiClientProps).getInstantMix({
params: { params: {
@@ -740,7 +746,7 @@ export const JellyfinController: ControllerEndpoint = {
SearchTerm: query.searchTerm, SearchTerm: query.searchTerm,
SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName', SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName',
SortOrder: sortOrderMap.jellyfin[query.sortOrder], SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.offset, StartIndex: query.startIndex,
...query._custom?.jellyfin, ...query._custom?.jellyfin,
Years: yearsFilter, Years: yearsFilter,
}, },
@@ -775,7 +781,7 @@ export const JellyfinController: ControllerEndpoint = {
SearchTerm: query.searchTerm, SearchTerm: query.searchTerm,
SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName', SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName',
SortOrder: sortOrderMap.jellyfin[query.sortOrder], SortOrder: sortOrderMap.jellyfin[query.sortOrder],
StartIndex: query.offset, StartIndex: query.startIndex,
...query._custom?.jellyfin, ...query._custom?.jellyfin,
Years: yearsFilter, Years: yearsFilter,
}, },
@@ -804,14 +810,14 @@ export const JellyfinController: ControllerEndpoint = {
items: items.map((item) => items: items.map((item) =>
jfNormalize.song(item, apiClientProps.server, '', query.imageSize), jfNormalize.song(item, apiClientProps.server, '', query.imageSize),
), ),
offset: query.offset, startIndex: query.startIndex,
totalRecordCount, totalRecordCount,
}; };
}, },
getSongListCount: async ({ apiClientProps, query }) => getSongListCount: async ({ apiClientProps, query }) =>
JellyfinController.getSongList({ JellyfinController.getSongList({
apiClientProps, apiClientProps,
query: { ...query, limit: 1, offset: 0 }, query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!), }).then((result) => result!.totalRecordCount!),
getTags: async (args) => { getTags: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -867,7 +873,7 @@ export const JellyfinController: ControllerEndpoint = {
return { return {
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')), items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
offset: 0, startIndex: 0,
totalRecordCount: res.body.TotalRecordCount, totalRecordCount: res.body.TotalRecordCount,
}; };
}, },
+1 -1
View File
@@ -11,7 +11,7 @@ import { useAuthStore } from '/@/renderer/store';
import { ndType } from '/@/shared/api/navidrome/navidrome-types'; import { ndType } from '/@/shared/api/navidrome/navidrome-types';
import { resultWithHeaders } from '/@/shared/api/utils'; import { resultWithHeaders } from '/@/shared/api/utils';
import { toast } from '/@/shared/components/toast/toast'; import { toast } from '/@/shared/components/toast/toast';
import { ServerListItem } from '/@/shared/types/domain/server-domain-types'; import { ServerListItem } from '/@/shared/types/domain-types';
const localSettings = isElectron() ? window.api.localSettings : null; const localSettings = isElectron() ? window.api.localSettings : null;
@@ -3,28 +3,28 @@ import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller'; import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
import { NDSongListSort } from '/@/shared/api/navidrome.types'; import { NDSongListSort } from '/@/shared/api/navidrome.types';
import { ndNormalize } from '/@/shared/api/navidrome/navidrome-normalize'; import { ndNormalize } from '/@/shared/api/navidrome/navidrome-normalize';
import { ndType } from '/@/shared/api/navidrome/navidrome-types'; import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
import { normalize } from '/@/shared/api/subsonic/subsonic-normalize';
import { SubsonicExtensions } from '/@/shared/api/subsonic/subsonic-types';
import { getFeatures, hasFeature, VersionInfo } from '/@/shared/api/utils'; import { getFeatures, hasFeature, VersionInfo } from '/@/shared/api/utils';
import { albumListSortMap } from '/@/shared/types/domain/album-domain-types';
import { ControllerEndpoint } from '/@/shared/types/domain/api-domain-types';
import { albumArtistListSortMap } from '/@/shared/types/domain/artist-domain-types';
import { AuthenticationResponse } from '/@/shared/types/domain/auth-domain-types';
import { import {
PlaylistSongListRequest, albumArtistListSortMap,
albumListSortMap,
AuthenticationResponse,
ControllerEndpoint,
genreListSortMap,
playlistListSortMap,
PlaylistSongListArgs,
PlaylistSongListResponse, PlaylistSongListResponse,
} from '/@/shared/types/domain/playlist-domain-types';
import {
ServerFeature,
ServerFeatures,
ServerListItem, ServerListItem,
} from '/@/shared/types/domain/server-domain-types'; Song,
import { sortOrderMap } from '/@/shared/types/domain/shared-domain-types'; songListSortMap,
import { Song, songListSortMap } from '/@/shared/types/domain/song-domain-types'; sortOrderMap,
userListSortMap,
} from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
const VERSION_INFO: VersionInfo = [ const VERSION_INFO: VersionInfo = [
['0.55.0', { [ServerFeature.BFR]: [1] }], ['0.56.0', { [ServerFeature.TRACK_ALBUM_ARTIST_SEARCH]: [1] }],
['0.55.0', { [ServerFeature.BFR]: [1], [ServerFeature.TAGS]: [1] }],
['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }], ['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }], ['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
]; ];
@@ -55,6 +55,9 @@ const excludeMissing = (server: null | ServerListItem) => {
return undefined; return undefined;
}; };
const getArtistSongKey = (server: null | ServerListItem) =>
hasFeature(server, ServerFeature.TRACK_ALBUM_ARTIST_SEARCH) ? 'artists_id' : 'album_artist_id';
export const NavidromeController: ControllerEndpoint = { export const NavidromeController: ControllerEndpoint = {
addToPlaylist: async (args) => { addToPlaylist: async (args) => {
const { apiClientProps, body, query } = args; const { apiClientProps, body, query } = args;
@@ -268,15 +271,19 @@ export const NavidromeController: ControllerEndpoint = {
getAlbumList: async (args) => { getAlbumList: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
const genres = hasFeature(apiClientProps.server, ServerFeature.BFR)
? query.genres
: query.genres?.[0];
const res = await ndApiClient(apiClientProps).getAlbumList({ const res = await ndApiClient(apiClientProps).getAlbumList({
query: { query: {
_end: query.offset + (query.limit || 0), _end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder], _order: sortOrderMap.navidrome[query.sortOrder],
_sort: albumListSortMap.navidrome[query.sortBy], _sort: albumListSortMap.navidrome[query.sortBy],
_start: query.offset, _start: query.startIndex,
artist_id: query.artistIds?.[0], artist_id: query.artistIds?.[0],
compilation: query.compilation, compilation: query.compilation,
genre_id: query.genres?.[0], genre_id: genres,
name: query.searchTerm, name: query.searchTerm,
...query._custom?.navidrome, ...query._custom?.navidrome,
starred: query.favorite, starred: query.favorite,
@@ -290,24 +297,24 @@ export const NavidromeController: ControllerEndpoint = {
return { return {
items: res.body.data.map((album) => ndNormalize.album(album, apiClientProps.server)), items: res.body.data.map((album) => ndNormalize.album(album, apiClientProps.server)),
offset: query?.offset || 0, startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
}; };
}, },
getAlbumListCount: async ({ apiClientProps, query }) => getAlbumListCount: async ({ apiClientProps, query }) =>
NavidromeController.getAlbumList({ NavidromeController.getAlbumList({
apiClientProps, apiClientProps,
query: { ...query, limit: 1, offset: 0 }, query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!), }).then((result) => result!.totalRecordCount!),
getArtistList: async (args) => { getArtistList: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getAlbumArtistList({ const res = await ndApiClient(apiClientProps).getAlbumArtistList({
query: { query: {
_end: query.offset + (query.limit || 0), _end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder], _order: sortOrderMap.navidrome[query.sortOrder],
_sort: albumArtistListSortMap.navidrome[query.sortBy], _sort: albumArtistListSortMap.navidrome[query.sortBy],
_start: query.offset, _start: query.startIndex,
name: query.searchTerm, name: query.searchTerm,
...query._custom?.navidrome, ...query._custom?.navidrome,
role: query.role || undefined, role: query.role || undefined,
@@ -332,14 +339,14 @@ export const NavidromeController: ControllerEndpoint = {
apiClientProps.server, apiClientProps.server,
), ),
), ),
offset: query.offset, startIndex: query.startIndex,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
}; };
}, },
getArtistListCount: async ({ apiClientProps, query }) => getArtistListCount: async ({ apiClientProps, query }) =>
NavidromeController.getArtistList({ NavidromeController.getArtistList({
apiClientProps, apiClientProps,
query: { ...query, limit: 1, offset: 0 }, query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!), }).then((result) => result!.totalRecordCount!),
getDownloadUrl: SubsonicController.getDownloadUrl, getDownloadUrl: SubsonicController.getDownloadUrl,
getGenreList: async (args) => { getGenreList: async (args) => {
@@ -347,10 +354,10 @@ export const NavidromeController: ControllerEndpoint = {
const res = await ndApiClient(apiClientProps).getGenreList({ const res = await ndApiClient(apiClientProps).getGenreList({
query: { query: {
_end: query.offset + (query.limit || 0), _end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder], _order: sortOrderMap.navidrome[query.sortOrder],
_sort: genreListSortMap.navidrome[query.sortBy], _sort: genreListSortMap.navidrome[query.sortBy],
_start: query.offset, _start: query.startIndex,
name: query.searchTerm, name: query.searchTerm,
}, },
}); });
@@ -361,7 +368,7 @@ export const NavidromeController: ControllerEndpoint = {
return { return {
items: res.body.data.map((genre) => ndNormalize.genre(genre)), items: res.body.data.map((genre) => ndNormalize.genre(genre)),
offset: query.offset || 0, startIndex: query.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
}; };
}, },
@@ -397,10 +404,10 @@ export const NavidromeController: ControllerEndpoint = {
const res = await ndApiClient(apiClientProps).getPlaylistList({ const res = await ndApiClient(apiClientProps).getPlaylistList({
query: { query: {
_end: query.offset + (query.limit || 0), _end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder], _order: sortOrderMap.navidrome[query.sortOrder],
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined, _sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
_start: query.offset, _start: query.startIndex,
q: query.searchTerm, q: query.searchTerm,
...customQuery, ...customQuery,
}, },
@@ -412,18 +419,16 @@ export const NavidromeController: ControllerEndpoint = {
return { return {
items: res.body.data.map((item) => ndNormalize.playlist(item, apiClientProps.server)), items: res.body.data.map((item) => ndNormalize.playlist(item, apiClientProps.server)),
offset: query?.offset || 0, startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
}; };
}, },
getPlaylistListCount: async ({ apiClientProps, query }) => getPlaylistListCount: async ({ apiClientProps, query }) =>
NavidromeController.getPlaylistList({ NavidromeController.getPlaylistList({
apiClientProps, apiClientProps,
query: { ...query, limit: 1, offset: 0 }, query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!), }).then((result) => result!.totalRecordCount!),
getPlaylistSongList: async ( getPlaylistSongList: async (args: PlaylistSongListArgs): Promise<PlaylistSongListResponse> => {
args: PlaylistSongListRequest,
): Promise<PlaylistSongListResponse> => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getPlaylistSongList({ const res = await ndApiClient(apiClientProps).getPlaylistSongList({
@@ -431,12 +436,9 @@ export const NavidromeController: ControllerEndpoint = {
id: query.id, id: query.id,
}, },
query: { query: {
_end: query.startIndex + (query.limit || -1), _end: -1,
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : 'ASC', _order: 'ASC',
_sort: query.sortBy _start: 0,
? songListSortMap.navidrome[query.sortBy]
: ndType._enum.songList.ID,
_start: query.startIndex,
...excludeMissing(apiClientProps.server), ...excludeMissing(apiClientProps.server),
}, },
}); });
@@ -447,7 +449,7 @@ export const NavidromeController: ControllerEndpoint = {
return { return {
items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server)), items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server)),
offset: query?.startIndex || 0, startIndex: 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
}; };
}, },
@@ -468,38 +470,20 @@ export const NavidromeController: ControllerEndpoint = {
ping.body.serverVersion = '0.55.0'; ping.body.serverVersion = '0.55.0';
} }
const navidromeFeatures: Record<string, number[]> = getFeatures( const navidromeFeatures = getFeatures(VERSION_INFO, ping.body.serverVersion!);
VERSION_INFO, const subsonicArgs = await SubsonicController.getServerInfo(args);
ping.body.serverVersion!,
);
if (ping.body.openSubsonic) { const features = {
const res = await ssApiClient(apiClientProps).getServerInfo(); ...navidromeFeatures,
...subsonicArgs.features,
if (res.status !== 200) {
throw new Error('Failed to get server extensions');
}
// The type here isn't necessarily an array (even though it's supposed to be). This is
// an implementation detail of Navidrome 0.50. Do a type check to make sure it's actually
// an array, and not an empty object.
if (Array.isArray(res.body.openSubsonicExtensions)) {
for (const extension of res.body.openSubsonicExtensions) {
navidromeFeatures[extension.name] = extension.versions;
}
}
}
const features: ServerFeatures = {
bfr: navidromeFeatures[ServerFeature.BFR],
lyricsMultipleStructured: navidromeFeatures[SubsonicExtensions.SONG_LYRICS],
playlistsSmart: navidromeFeatures[ServerFeature.PLAYLISTS_SMART],
publicPlaylist: [1], publicPlaylist: [1],
sharingAlbumSong: navidromeFeatures[ServerFeature.SHARING_ALBUM_SONG],
tags: navidromeFeatures[ServerFeature.BFR],
}; };
return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion! }; return {
features,
id: apiClientProps.server?.id,
version: ping.body.serverVersion!,
};
}, },
getSimilarSongs: async (args) => { getSimilarSongs: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -519,7 +503,7 @@ export const NavidromeController: ControllerEndpoint = {
if (res.status === 200 && res.body.similarSongs?.song) { if (res.status === 200 && res.body.similarSongs?.song) {
const similar = res.body.similarSongs.song.reduce<Song[]>((acc, song) => { const similar = res.body.similarSongs.song.reduce<Song[]>((acc, song) => {
if (song.id !== query.songId) { if (song.id !== query.songId) {
acc.push(normalize.song(song, apiClientProps.server)); acc.push(ssNormalize.song(song, apiClientProps.server));
} }
return acc; return acc;
@@ -536,7 +520,7 @@ export const NavidromeController: ControllerEndpoint = {
_order: 'ASC', _order: 'ASC',
_sort: NDSongListSort.RANDOM, _sort: NDSongListSort.RANDOM,
_start: 0, _start: 0,
album_artist_id: query.albumArtistIds, [getArtistSongKey(apiClientProps.server)]: query.albumArtistIds,
...excludeMissing(apiClientProps.server), ...excludeMissing(apiClientProps.server),
}, },
}); });
@@ -573,14 +557,13 @@ export const NavidromeController: ControllerEndpoint = {
const res = await ndApiClient(apiClientProps).getSongList({ const res = await ndApiClient(apiClientProps).getSongList({
query: { query: {
_end: query.offset + (query.limit || -1), _end: query.startIndex + (query.limit || -1),
_order: sortOrderMap.navidrome[query.sortOrder], _order: sortOrderMap.navidrome[query.sortOrder],
_sort: songListSortMap.navidrome[query.sortBy], _sort: songListSortMap.navidrome[query.sortBy],
_start: query.offset, _start: query.startIndex,
album_artist_id: query.albumArtistIds,
album_id: query.albumIds, album_id: query.albumIds,
artist_id: query.artistIds,
genre_id: query.genreIds, genre_id: query.genreIds,
[getArtistSongKey(apiClientProps.server)]: query.artistIds ?? query.albumArtistIds,
starred: query.favorite, starred: query.favorite,
title: query.searchTerm, title: query.searchTerm,
...query._custom?.navidrome, ...query._custom?.navidrome,
@@ -596,14 +579,14 @@ export const NavidromeController: ControllerEndpoint = {
items: res.body.data.map((song) => items: res.body.data.map((song) =>
ndNormalize.song(song, apiClientProps.server, query.imageSize), ndNormalize.song(song, apiClientProps.server, query.imageSize),
), ),
offset: query?.offset || 0, startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
}; };
}, },
getSongListCount: async ({ apiClientProps, query }) => getSongListCount: async ({ apiClientProps, query }) =>
NavidromeController.getSongList({ NavidromeController.getSongList({
apiClientProps, apiClientProps,
query: { ...query, limit: 1, offset: 0 }, query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!), }).then((result) => result!.totalRecordCount!),
getStructuredLyrics: SubsonicController.getStructuredLyrics, getStructuredLyrics: SubsonicController.getStructuredLyrics,
getTags: async (args) => { getTags: async (args) => {
@@ -652,10 +635,10 @@ export const NavidromeController: ControllerEndpoint = {
const res = await ndApiClient(apiClientProps).getUserList({ const res = await ndApiClient(apiClientProps).getUserList({
query: { query: {
_end: query.offset + (query.limit || 0), _end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder], _order: sortOrderMap.navidrome[query.sortOrder],
_sort: userListSortMap.navidrome[query.sortBy], _sort: userListSortMap.navidrome[query.sortBy],
_start: query.offset, _start: query.startIndex,
...query._custom?.navidrome, ...query._custom?.navidrome,
}, },
}); });
@@ -666,7 +649,7 @@ export const NavidromeController: ControllerEndpoint = {
return { return {
items: res.body.data.map((user) => ndNormalize.user(user)), items: res.body.data.map((user) => ndNormalize.user(user)),
offset: query?.offset || 0, startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
}; };
}, },
+12 -41
View File
@@ -1,31 +1,26 @@
import { QueryFunctionContext } from '@tanstack/react-query'; import type {
import { AlbumDetailQuery, AlbumListQuery } from '/@/shared/types/domain/album-domain-types';
import {
AlbumArtistDetailQuery, AlbumArtistDetailQuery,
AlbumArtistListQuery, AlbumArtistListQuery,
AlbumDetailQuery,
AlbumListQuery,
ArtistListQuery, ArtistListQuery,
} from '/@/shared/types/domain/artist-domain-types'; GenreListQuery,
import { GenreListQuery } from '/@/shared/types/domain/genre-domain-types';
import {
LyricSearchQuery, LyricSearchQuery,
LyricSource,
LyricsQuery, LyricsQuery,
} from '/@/shared/types/domain/lyric-domain-types';
import {
PlaylistDetailQuery, PlaylistDetailQuery,
PlaylistListQuery, PlaylistListQuery,
PlaylistSongListQuery,
} from '/@/shared/types/domain/playlist-domain-types';
import { SearchQuery } from '/@/shared/types/domain/search-domain-types';
import {
RandomSongListQuery, RandomSongListQuery,
SearchQuery,
SimilarSongsQuery, SimilarSongsQuery,
SongDetailQuery, SongDetailQuery,
SongListQuery, SongListQuery,
TopSongListQuery, TopSongListQuery,
} from '/@/shared/types/domain/song-domain-types'; UserListQuery,
import { UserListQuery } from '/@/shared/types/domain/user-domain-types'; } from '/@/shared/types/domain-types';
import { QueryFunctionContext } from '@tanstack/react-query';
import { LyricSource } from '/@/shared/types/domain-types';
export const splitPaginatedQuery = (key: any) => { export const splitPaginatedQuery = (key: any) => {
const { limit, startIndex, ...filter } = key || {}; const { limit, startIndex, ...filter } = key || {};
@@ -195,21 +190,6 @@ export const queryKeys: Record<
if (id) return [serverId, 'playlists', id, 'detail'] as const; if (id) return [serverId, 'playlists', id, 'detail'] as const;
return [serverId, 'playlists', 'detail'] as const; return [serverId, 'playlists', 'detail'] as const;
}, },
detailSongList: (serverId: string, id: string, query?: PlaylistSongListQuery) => {
const { filter, pagination } = splitPaginatedQuery(query);
if (query && id && pagination) {
return [serverId, 'playlists', id, 'detailSongList', filter, pagination] as const;
}
if (query && id) {
return [serverId, 'playlists', id, 'detailSongList', filter] as const;
}
if (id) return [serverId, 'playlists', id, 'detailSongList'] as const;
return [serverId, 'playlists', 'detailSongList'] as const;
},
list: (serverId: string, query?: PlaylistListQuery) => { list: (serverId: string, query?: PlaylistListQuery) => {
const { filter, pagination } = splitPaginatedQuery(query); const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) { if (query && pagination) {
@@ -223,16 +203,7 @@ export const queryKeys: Record<
return [serverId, 'playlists', 'list'] as const; return [serverId, 'playlists', 'list'] as const;
}, },
root: (serverId: string) => [serverId, 'playlists'] as const, root: (serverId: string) => [serverId, 'playlists'] as const,
songList: (serverId: string, id?: string, query?: PlaylistSongListQuery) => { songList: (serverId: string, id?: string) => {
const { filter, pagination } = splitPaginatedQuery(query);
if (query && id && pagination) {
return [serverId, 'playlists', id, 'songList', filter, pagination] as const;
}
if (query && id) {
return [serverId, 'playlists', id, 'songList', filter] as const;
}
if (id) return [serverId, 'playlists', id, 'songList'] as const; if (id) return [serverId, 'playlists', id, 'songList'] as const;
return [serverId, 'playlists', 'songList'] as const; return [serverId, 'playlists', 'songList'] as const;
}, },
+1 -1
View File
@@ -7,7 +7,7 @@ import { z } from 'zod';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { ssType } from '/@/shared/api/subsonic/subsonic-types'; import { ssType } from '/@/shared/api/subsonic/subsonic-types';
import { toast } from '/@/shared/components/toast/toast'; import { toast } from '/@/shared/components/toast/toast';
import { ServerListItem } from '/@/shared/types/domain/server-domain-types'; import { ServerListItem } from '/@/shared/types/domain-types';
const c = initContract(); const c = initContract();
@@ -8,18 +8,25 @@ import { z } from 'zod';
import { contract, ssApiClient } from '/@/renderer/api/subsonic/subsonic-api'; import { contract, ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
import { randomString } from '/@/renderer/utils'; import { randomString } from '/@/renderer/utils';
import { normalize } from '/@/shared/api/subsonic/subsonic-normalize'; import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize';
import { import {
AlbumListSortType, AlbumListSortType,
ssType, ssType,
SubsonicExtensions, SubsonicExtensions,
} from '/@/shared/api/subsonic/subsonic-types'; } from '/@/shared/api/subsonic/subsonic-types';
import { AlbumListSort, sortAlbumList } from '/@/shared/types/domain/album-domain-types'; import {
import { ControllerEndpoint } from '/@/shared/types/domain/api-domain-types'; AlbumListSort,
import { sortAlbumArtistList } from '/@/shared/types/domain/artist-domain-types'; ControllerEndpoint,
import { ServerFeatures } from '/@/shared/types/domain/server-domain-types'; GenreListSort,
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; LibraryItem,
import { Song, sortSongList } from '/@/shared/types/domain/song-domain-types'; PlaylistListSort,
Song,
sortAlbumArtistList,
sortAlbumList,
SortOrder,
sortSongList,
} from '/@/shared/types/domain-types';
import { ServerFeatures } from '/@/shared/types/features-types';
const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefined> = { const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefined> = {
[AlbumListSort.ALBUM_ARTIST]: AlbumListSortType.ALPHABETICAL_BY_ARTIST, [AlbumListSort.ALBUM_ARTIST]: AlbumListSortType.ALPHABETICAL_BY_ARTIST,
@@ -34,7 +41,7 @@ const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefin
[AlbumListSort.RATING]: undefined, [AlbumListSort.RATING]: undefined,
[AlbumListSort.RECENTLY_ADDED]: AlbumListSortType.NEWEST, [AlbumListSort.RECENTLY_ADDED]: AlbumListSortType.NEWEST,
[AlbumListSort.RECENTLY_PLAYED]: AlbumListSortType.RECENT, [AlbumListSort.RECENTLY_PLAYED]: AlbumListSortType.RECENT,
[AlbumListSort.RELEASE_DATE]: undefined, [AlbumListSort.RELEASE_DATE]: AlbumListSortType.BY_YEAR,
[AlbumListSort.SONG_COUNT]: undefined, [AlbumListSort.SONG_COUNT]: undefined,
[AlbumListSort.YEAR]: AlbumListSortType.BY_YEAR, [AlbumListSort.YEAR]: AlbumListSortType.BY_YEAR,
}; };
@@ -198,11 +205,11 @@ export const SubsonicController: ControllerEndpoint = {
} }
return { return {
...normalize.albumArtist(artist, apiClientProps.server, 300), ...ssNormalize.albumArtist(artist, apiClientProps.server, 300),
albums: artist.album?.map((album) => normalize.album(album, apiClientProps.server)), albums: artist.album?.map((album) => ssNormalize.album(album, apiClientProps.server)),
similarArtists: similarArtists:
artistInfo?.similarArtist?.map((artist) => artistInfo?.similarArtist?.map((artist) =>
normalize.albumArtist(artist, apiClientProps.server, 300), ssNormalize.albumArtist(artist, apiClientProps.server, 300),
) || null, ) || null,
}; };
}, },
@@ -222,7 +229,7 @@ export const SubsonicController: ControllerEndpoint = {
const artists = (res.body.artists?.index || []).flatMap((index) => index.artist); const artists = (res.body.artists?.index || []).flatMap((index) => index.artist);
let results = artists.map((artist) => let results = artists.map((artist) =>
normalize.albumArtist(artist, apiClientProps.server, 300), ssNormalize.albumArtist(artist, apiClientProps.server, 300),
); );
if (query.searchTerm) { if (query.searchTerm) {
@@ -258,7 +265,7 @@ export const SubsonicController: ControllerEndpoint = {
throw new Error('Failed to get album detail'); throw new Error('Failed to get album detail');
} }
return normalize.album(res.body.album, apiClientProps.server); return ssNormalize.album(res.body.album, apiClientProps.server);
}, },
getAlbumList: async (args) => { getAlbumList: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -267,7 +274,7 @@ export const SubsonicController: ControllerEndpoint = {
const res = await ssApiClient(apiClientProps).search3({ const res = await ssApiClient(apiClientProps).search3({
query: { query: {
albumCount: query.limit, albumCount: query.limit,
albumOffset: query.offset, albumOffset: query.startIndex,
artistCount: 0, artistCount: 0,
artistOffset: 0, artistOffset: 0,
query: query.searchTerm || '', query: query.searchTerm || '',
@@ -282,12 +289,12 @@ export const SubsonicController: ControllerEndpoint = {
const results = const results =
res.body.searchResult3?.album?.map((album) => res.body.searchResult3?.album?.map((album) =>
normalize.album(album, apiClientProps.server), ssNormalize.album(album, apiClientProps.server),
) || []; ) || [];
return { return {
items: results, items: results,
offset: query.offset, startIndex: query.startIndex,
totalRecordCount: null, totalRecordCount: null,
}; };
} }
@@ -317,11 +324,11 @@ export const SubsonicController: ControllerEndpoint = {
return artist.body.artist.album ?? []; return artist.body.artist.album ?? [];
}); });
const items = albums.map((album) => normalize.album(album, apiClientProps.server)); const items = albums.map((album) => ssNormalize.album(album, apiClientProps.server));
return { return {
items: sortAlbumList(items, query.sortBy, query.sortOrder), items: sortAlbumList(items, query.sortBy, query.sortOrder),
offset: 0, startIndex: 0,
totalRecordCount: albums.length, totalRecordCount: albums.length,
}; };
} }
@@ -339,12 +346,12 @@ export const SubsonicController: ControllerEndpoint = {
const results = const results =
res.body.starred?.album?.map((album) => res.body.starred?.album?.map((album) =>
normalize.album(album, apiClientProps.server), ssNormalize.album(album, apiClientProps.server),
) || []; ) || [];
return { return {
items: sortAlbumList(results, query.sortBy, query.sortOrder), items: sortAlbumList(results, query.sortBy, query.sortOrder),
offset: 0, startIndex: 0,
totalRecordCount: res.body.starred?.album?.length || 0, totalRecordCount: res.body.starred?.album?.length || 0,
}; };
} }
@@ -374,7 +381,7 @@ export const SubsonicController: ControllerEndpoint = {
} }
if (type === AlbumListSortType.BY_YEAR && !fromYear && !toYear) { if (type === AlbumListSortType.BY_YEAR && !fromYear && !toYear) {
if (query.sortOrder === ListSortOrder.ASC) { if (query.sortOrder === SortOrder.ASC) {
fromYear = 0; fromYear = 0;
toYear = dayjs().year(); toYear = dayjs().year();
} else { } else {
@@ -388,7 +395,7 @@ export const SubsonicController: ControllerEndpoint = {
fromYear, fromYear,
genre: query.genres?.length ? query.genres[0] : undefined, genre: query.genres?.length ? query.genres[0] : undefined,
musicFolderId: query.musicFolderId, musicFolderId: query.musicFolderId,
offset: query.offset, offset: query.startIndex,
size: query.limit, size: query.limit,
toYear, toYear,
type, type,
@@ -402,9 +409,9 @@ export const SubsonicController: ControllerEndpoint = {
return { return {
items: items:
res.body.albumList2.album?.map((album) => res.body.albumList2.album?.map((album) =>
normalize.album(album, apiClientProps.server, 300), ssNormalize.album(album, apiClientProps.server, 300),
) || [], ) || [],
offset: query.offset, startIndex: query.startIndex,
totalRecordCount: null, totalRecordCount: null,
}; };
}, },
@@ -572,7 +579,7 @@ export const SubsonicController: ControllerEndpoint = {
} }
let results = artists.map((artist) => let results = artists.map((artist) =>
normalize.albumArtist(artist, apiClientProps.server, 300), ssNormalize.albumArtist(artist, apiClientProps.server, 300),
); );
if (query.searchTerm) { if (query.searchTerm) {
@@ -589,7 +596,7 @@ export const SubsonicController: ControllerEndpoint = {
return { return {
items: results, items: results,
offset: query.offset, startIndex: query.startIndex,
totalRecordCount: results?.length || 0, totalRecordCount: results?.length || 0,
}; };
}, },
@@ -633,11 +640,11 @@ export const SubsonicController: ControllerEndpoint = {
break; break;
} }
const genres = results.map(normalize.genre); const genres = results.map(ssNormalize.genre);
return { return {
items: genres, items: genres,
offset: 0, startIndex: 0,
totalRecordCount: genres.length, totalRecordCount: genres.length,
}; };
}, },
@@ -655,7 +662,7 @@ export const SubsonicController: ControllerEndpoint = {
id: folder.id.toString(), id: folder.id.toString(),
name: folder.name, name: folder.name,
})), })),
offset: 0, startIndex: 0,
totalRecordCount: res.body.musicFolders.musicFolder.length, totalRecordCount: res.body.musicFolders.musicFolder.length,
}; };
}, },
@@ -672,7 +679,7 @@ export const SubsonicController: ControllerEndpoint = {
throw new Error('Failed to get playlist detail'); throw new Error('Failed to get playlist detail');
} }
return normalize.playlist(res.body.playlist, apiClientProps.server); return ssNormalize.playlist(res.body.playlist, apiClientProps.server);
}, },
getPlaylistList: async ({ apiClientProps, query }) => { getPlaylistList: async ({ apiClientProps, query }) => {
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc'; const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
@@ -717,8 +724,8 @@ export const SubsonicController: ControllerEndpoint = {
} }
return { return {
items: results.map((playlist) => normalize.playlist(playlist, apiClientProps.server)), items: results.map((playlist) => ssNormalize.playlist(playlist, apiClientProps.server)),
offset: 0, startIndex: 0,
totalRecordCount: results.length, totalRecordCount: results.length,
}; };
}, },
@@ -752,18 +759,14 @@ export const SubsonicController: ControllerEndpoint = {
throw new Error('Failed to get playlist song list'); throw new Error('Failed to get playlist song list');
} }
let results = const items =
res.body.playlist.entry?.map((song) => normalize.song(song, apiClientProps.server)) || res.body.playlist.entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) ||
[]; [];
if (query.sortBy && query.sortOrder) {
results = sortSongList(results, query.sortBy, query.sortOrder);
}
return { return {
items: results, items,
offset: 0, startIndex: 0,
totalRecordCount: results?.length || 0, totalRecordCount: items.length,
}; };
}, },
getRandomSongList: async (args) => { getRandomSongList: async (args) => {
@@ -786,8 +789,8 @@ export const SubsonicController: ControllerEndpoint = {
const results = res.body.randomSongs?.song || []; const results = res.body.randomSongs?.song || [];
return { return {
items: results.map((song) => normalize.song(song, apiClientProps.server)), items: results.map((song) => ssNormalize.song(song, apiClientProps.server)),
offset: 0, startIndex: 0,
totalRecordCount: res.body.randomSongs?.song?.length || 0, totalRecordCount: res.body.randomSongs?.song?.length || 0,
}; };
}, },
@@ -871,7 +874,7 @@ export const SubsonicController: ControllerEndpoint = {
return res.body.similarSongs.song.reduce<Song[]>((acc, song) => { return res.body.similarSongs.song.reduce<Song[]>((acc, song) => {
if (song.id !== query.songId) { if (song.id !== query.songId) {
acc.push(normalize.song(song, apiClientProps.server)); acc.push(ssNormalize.song(song, apiClientProps.server));
} }
return acc; return acc;
@@ -890,7 +893,7 @@ export const SubsonicController: ControllerEndpoint = {
throw new Error('Failed to get song detail'); throw new Error('Failed to get song detail');
} }
return normalize.song(res.body.song, apiClientProps.server); return ssNormalize.song(res.body.song, apiClientProps.server);
}, },
getSongList: async ({ apiClientProps, query }) => { getSongList: async ({ apiClientProps, query }) => {
const fromAlbumPromises: Promise<ServerInferResponses<typeof contract.getAlbum>>[] = []; const fromAlbumPromises: Promise<ServerInferResponses<typeof contract.getAlbum>>[] = [];
@@ -905,7 +908,7 @@ export const SubsonicController: ControllerEndpoint = {
artistOffset: 0, artistOffset: 0,
query: query.searchTerm || '', query: query.searchTerm || '',
songCount: query.limit, songCount: query.limit,
songOffset: query.offset, songOffset: query.startIndex,
}, },
}); });
@@ -916,9 +919,9 @@ export const SubsonicController: ControllerEndpoint = {
return { return {
items: items:
res.body.searchResult3?.song?.map((song) => res.body.searchResult3?.song?.map((song) =>
normalize.song(song, apiClientProps.server), ssNormalize.song(song, apiClientProps.server),
) || [], ) || [],
offset: query.offset, startIndex: query.startIndex,
totalRecordCount: null, totalRecordCount: null,
}; };
} }
@@ -929,7 +932,7 @@ export const SubsonicController: ControllerEndpoint = {
count: query.limit, count: query.limit,
genre: query.genreIds[0], genre: query.genreIds[0],
musicFolderId: query.musicFolderId, musicFolderId: query.musicFolderId,
offset: query.offset, offset: query.startIndex,
}, },
}); });
@@ -940,8 +943,8 @@ export const SubsonicController: ControllerEndpoint = {
const results = res.body.songsByGenre?.song || []; const results = res.body.songsByGenre?.song || [];
return { return {
items: results.map((song) => normalize.song(song, apiClientProps.server)) || [], items: results.map((song) => ssNormalize.song(song, apiClientProps.server)) || [],
offset: 0, startIndex: 0,
totalRecordCount: null, totalRecordCount: null,
}; };
} }
@@ -959,12 +962,12 @@ export const SubsonicController: ControllerEndpoint = {
const results = const results =
(res.body.starred?.song || []).map((song) => (res.body.starred?.song || []).map((song) =>
normalize.song(song, apiClientProps.server), ssNormalize.song(song, apiClientProps.server),
) || []; ) || [];
return { return {
items: sortSongList(results, query.sortBy, query.sortOrder), items: sortSongList(results, query.sortBy, query.sortOrder),
offset: 0, startIndex: 0,
totalRecordCount: (res.body.starred?.song || []).length || 0, totalRecordCount: (res.body.starred?.song || []).length || 0,
}; };
} }
@@ -1033,8 +1036,8 @@ export const SubsonicController: ControllerEndpoint = {
} }
return { return {
items: results.map((song) => normalize.song(song, apiClientProps.server)), items: results.map((song) => ssNormalize.song(song, apiClientProps.server)),
offset: 0, startIndex: 0,
totalRecordCount: results.length, totalRecordCount: results.length,
}; };
} }
@@ -1047,7 +1050,7 @@ export const SubsonicController: ControllerEndpoint = {
artistOffset: 0, artistOffset: 0,
query: query.searchTerm || '', query: query.searchTerm || '',
songCount: query.limit, songCount: query.limit,
songOffset: query.offset, songOffset: query.startIndex,
}, },
}); });
@@ -1058,9 +1061,9 @@ export const SubsonicController: ControllerEndpoint = {
return { return {
items: items:
res.body.searchResult3?.song?.map((song) => res.body.searchResult3?.song?.map((song) =>
normalize.song(song, apiClientProps.server), ssNormalize.song(song, apiClientProps.server),
) || [], ) || [],
offset: 0, startIndex: 0,
totalRecordCount: null, totalRecordCount: null,
}; };
}, },
@@ -1295,9 +1298,9 @@ export const SubsonicController: ControllerEndpoint = {
return { return {
items: items:
res.body.topSongs?.song?.map((song) => res.body.topSongs?.song?.map((song) =>
normalize.song(song, apiClientProps.server), ssNormalize.song(song, apiClientProps.server),
) || [], ) || [],
offset: 0, startIndex: 0,
totalRecordCount: res.body.topSongs?.song?.length || 0, totalRecordCount: res.body.topSongs?.song?.length || 0,
}; };
}, },
@@ -1365,13 +1368,13 @@ export const SubsonicController: ControllerEndpoint = {
return { return {
albumArtists: (res.body.searchResult3?.artist || [])?.map((artist) => albumArtists: (res.body.searchResult3?.artist || [])?.map((artist) =>
normalize.albumArtist(artist, apiClientProps.server), ssNormalize.albumArtist(artist, apiClientProps.server),
), ),
albums: (res.body.searchResult3?.album || []).map((album) => albums: (res.body.searchResult3?.album || []).map((album) =>
normalize.album(album, apiClientProps.server), ssNormalize.album(album, apiClientProps.server),
), ),
songs: (res.body.searchResult3?.song || []).map((song) => songs: (res.body.searchResult3?.song || []).map((song) =>
normalize.song(song, apiClientProps.server), ssNormalize.song(song, apiClientProps.server),
), ),
}; };
}, },
+10 -1
View File
@@ -190,7 +190,16 @@ export const App = () => {
return ( return (
<MantineProvider defaultColorScheme={mode as 'dark' | 'light'} theme={theme}> <MantineProvider defaultColorScheme={mode as 'dark' | 'light'} theme={theme}>
<Notifications containerWidth="300px" position="bottom-center" zIndex={50000} /> <Notifications
containerWidth="300px"
position="bottom-center"
styles={{
root: {
marginBottom: 90,
},
}}
zIndex={50000}
/>
<PlayQueueHandlerContext.Provider value={providerValue}> <PlayQueueHandlerContext.Provider value={providerValue}>
<WebAudioContext.Provider value={webAudioProvider}> <WebAudioContext.Provider value={webAudioProvider}>
<AppRouter /> <AppRouter />
+19 -21
View File
@@ -1,3 +1,4 @@
import type { Song } from '/@/shared/types/domain-types';
import type { CrossfadeStyle } from '/@/shared/types/types'; import type { CrossfadeStyle } from '/@/shared/types/types';
import type { ReactPlayerProps } from 'react-player'; import type { ReactPlayerProps } from 'react-player';
@@ -19,11 +20,9 @@ import {
gaplessHandler, gaplessHandler,
} from '/@/renderer/components/audio-player/utils/list-handlers'; } from '/@/renderer/components/audio-player/utils/list-handlers';
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio'; import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
import { TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store'; import { getServerById, TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store';
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store'; import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { toast } from '/@/shared/components/toast/toast'; import { toast } from '/@/shared/components/toast/toast';
import { QueueSong } from '/@/shared/types/domain/player-domain-types';
import { Song } from '/@/shared/types/domain/song-domain-types';
import { PlaybackStyle, PlayerStatus } from '/@/shared/types/types'; import { PlaybackStyle, PlayerStatus } from '/@/shared/types/types';
export type AudioPlayerProgress = { export type AudioPlayerProgress = {
@@ -58,28 +57,27 @@ const getDuration = (ref: any) => {
const EMPTY_SOURCE = const EMPTY_SOURCE =
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV'; 'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
const useSongUrl = ( const useSongUrl = (transcode: TranscodingConfig, current: boolean, song?: Song): null | string => {
transcode: TranscodingConfig,
current: boolean,
song?: QueueSong,
): null | string => {
const prior = useRef(['', '']); const prior = useRef(['', '']);
return useMemo(() => { return useMemo(() => {
if (song?._serverId) { if (song?.serverId) {
// If we are the current track, we do not want a transcoding // If we are the current track, we do not want a transcoding
// reconfiguration to force a restart. // reconfiguration to force a restart.
if (current && prior.current[0] === song._uniqueId) { if (current && prior.current[0] === song.uniqueId) {
return prior.current[1] as string; return prior.current[1];
} }
if (!transcode.enabled) { if (!transcode.enabled) {
// transcoding disabled; save the result // transcoding disabled; save the result
prior.current = [song._uniqueId, song.streamUrl]; prior.current = [song.uniqueId, song.streamUrl];
return song.streamUrl; return song.streamUrl;
} }
const result = api.controller.getTranscodingUrl({ const result = api.controller.getTranscodingUrl({
apiClientProps: {
server: getServerById(song.serverId),
},
query: { query: {
base: song.streamUrl, base: song.streamUrl,
...transcode, ...transcode,
@@ -87,14 +85,14 @@ const useSongUrl = (
})!; })!;
// transcoding enabled; save the updated result // transcoding enabled; save the updated result
prior.current = [song._uniqueId, result]; prior.current = [song.uniqueId, result];
return result; return result;
} }
// no track; clear result // no track; clear result
prior.current = ['', '']; prior.current = ['', ''];
return null; return null;
}, [song?._serverId, song?._uniqueId, song?.streamUrl, current, transcode]); }, [current, song?.uniqueId, song?.serverId, song?.streamUrl, transcode]);
}; };
export interface AudioPlayerRef { export interface AudioPlayerRef {
@@ -235,17 +233,17 @@ export const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>((props,
} }
const { error } = target; const { error } = target;
if (error?.code !== MediaError.MEDIA_ERR_DECODE) {
console.log('Playback error occurred:', error);
if (
error?.code !== MediaError.MEDIA_ERR_DECODE &&
error?.code !== MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
) {
return; return;
} }
const duration = player.getDuration();
const currentTime = player.getCurrentTime();
// Decode error within last second, handle as track ended
if (duration && duration - currentTime < 1) {
handleOnEnded(); handleOnEnded();
}
}; };
}; };
@@ -13,7 +13,7 @@ import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Button } from '/@/shared/components/button/button'; import { Button } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
export const CardControls = ({ export const CardControls = ({
+2 -5
View File
@@ -3,21 +3,18 @@ import formatDuration from 'format-duration';
import React from 'react'; import React from 'react';
import { generatePath } from 'react-router'; import { generatePath } from 'react-router';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Song } from 'src/main/features/core/lyrics/netease';
import styles from './card-rows.module.css'; import styles from './card-rows.module.css';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format'; import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { Album } from '/@/shared/types/domain/album-domain-types'; import { Album, AlbumArtist, Artist, Playlist, Song } from '/@/shared/types/domain-types';
import { Artist } from '/@/shared/types/domain/artist-domain-types';
import { Playlist } from '/@/shared/types/domain/playlist-domain-types';
import { CardRow } from '/@/shared/types/types'; import { CardRow } from '/@/shared/types/types';
interface CardRowsProps { interface CardRowsProps {
data: any; data: any;
rows: CardRow<Album>[] | CardRow<Artist>[]; rows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
} }
export const CardRows = ({ data, rows }: CardRowsProps) => { export const CardRows = ({ data, rows }: CardRowsProps) => {
+2 -4
View File
@@ -8,14 +8,12 @@ import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/
import { Image } from '/@/shared/components/image/image'; import { Image } from '/@/shared/components/image/image';
import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Album } from '/@/shared/types/domain/album-domain-types'; import { Album, AlbumArtist, Artist, LibraryItem } from '/@/shared/types/domain-types';
import { Artist } from '/@/shared/types/domain/artist-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types'; import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types';
interface BaseGridCardProps { interface BaseGridCardProps {
controls: { controls: {
cardRows: CardRow<Album>[] | CardRow<Artist>[]; cardRows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
handleFavorite: (options: { handleFavorite: (options: {
id: string[]; id: string[];
isFavorite: boolean; isFavorite: boolean;
@@ -20,8 +20,7 @@ import { Image } from '/@/shared/components/image/image';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { TextTitle } from '/@/shared/components/text-title/text-title'; import { TextTitle } from '/@/shared/components/text-title/text-title';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { Album } from '/@/shared/types/domain/album-domain-types'; import { Album, LibraryItem } from '/@/shared/types/domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
const variants: Variants = { const variants: Variants = {
@@ -24,9 +24,13 @@ import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { TextTitle } from '/@/shared/components/text-title/text-title'; import { TextTitle } from '/@/shared/components/text-title/text-title';
import { Album } from '/@/shared/types/domain/album-domain-types'; import {
import { Artist, RelatedArtist } from '/@/shared/types/domain/artist-domain-types'; Album,
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; AlbumArtist,
Artist,
LibraryItem,
RelatedArtist,
} from '/@/shared/types/domain-types';
import { CardRoute, CardRow } from '/@/shared/types/types'; import { CardRoute, CardRow } from '/@/shared/types/types';
const getSlidesPerView = (windowWidth: number) => { const getSlidesPerView = (windowWidth: number) => {
@@ -10,17 +10,20 @@ import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/
import { Image } from '/@/shared/components/image/image'; import { Image } from '/@/shared/components/image/image';
import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Album } from '/@/shared/types/domain/album-domain-types'; import {
import { Artist } from '/@/shared/types/domain/artist-domain-types'; Album,
import { Playlist } from '/@/shared/types/domain/playlist-domain-types'; AlbumArtist,
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; Artist,
import { Song } from '/@/shared/types/domain/song-domain-types'; LibraryItem,
Playlist,
Song,
} from '/@/shared/types/domain-types';
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types'; import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types';
interface BaseGridCardProps { interface BaseGridCardProps {
columnIndex: number; columnIndex: number;
controls: { controls: {
cardRows: CardRow<Album | Artist | Playlist | Song>[]; cardRows: CardRow<Album | AlbumArtist | Artist | Playlist | Song>[];
handleFavorite: (options: { handleFavorite: (options: {
id: string[]; id: string[];
isFavorite: boolean; isFavorite: boolean;
@@ -13,7 +13,7 @@ import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Button } from '/@/shared/components/button/button'; import { Button } from '/@/shared/components/button/button';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
import { Play, PlayQueueAddOptions } from '/@/shared/types/types'; import { Play, PlayQueueAddOptions } from '/@/shared/types/types';
export const GridCardControls = ({ export const GridCardControls = ({
@@ -10,17 +10,20 @@ import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/
import { Image } from '/@/shared/components/image/image'; import { Image } from '/@/shared/components/image/image';
import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Album } from '/@/shared/types/domain/album-domain-types'; import {
import { Artist } from '/@/shared/types/domain/artist-domain-types'; Album,
import { Playlist } from '/@/shared/types/domain/playlist-domain-types'; AlbumArtist,
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; Artist,
import { Song } from '/@/shared/types/domain/song-domain-types'; LibraryItem,
Playlist,
Song,
} from '/@/shared/types/domain-types';
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types'; import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/shared/types/types';
interface BaseGridCardProps { interface BaseGridCardProps {
columnIndex: number; columnIndex: number;
controls: { controls: {
cardRows: CardRow<Album | Artist | Playlist | Song>[]; cardRows: CardRow<Album | AlbumArtist | Artist | Playlist | Song>[];
handleFavorite: (options: { handleFavorite: (options: {
id: string[]; id: string[];
isFavorite: boolean; isFavorite: boolean;
@@ -14,9 +14,7 @@ import { FixedSizeList } from 'react-window';
import styles from './virtual-grid-wrapper.module.css'; import styles from './virtual-grid-wrapper.module.css';
import { GridCard } from '/@/renderer/components/virtual-grid/grid-card'; import { GridCard } from '/@/renderer/components/virtual-grid/grid-card';
import { Album } from '/@/shared/types/domain/album-domain-types'; import { Album, AlbumArtist, Artist, LibraryItem } from '/@/shared/types/domain-types';
import { Artist } from '/@/shared/types/domain/artist-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
const createItemData = memoize( const createItemData = memoize(
( (
@@ -74,7 +72,7 @@ export const VirtualGridWrapper = ({
width, width,
...rest ...rest
}: Omit<FixedSizeListProps, 'children' | 'height' | 'itemSize' | 'ref' | 'width'> & { }: Omit<FixedSizeListProps, 'children' | 'height' | 'itemSize' | 'ref' | 'width'> & {
cardRows: CardRow<Album | Artist>[]; cardRows: CardRow<Album | AlbumArtist | Artist>[];
columnCount: number; columnCount: number;
display: ListDisplayType; display: ListDisplayType;
handleFavorite?: (options: { handleFavorite?: (options: {
@@ -14,8 +14,7 @@ import {
import InfiniteLoader from 'react-window-infinite-loader'; import InfiniteLoader from 'react-window-infinite-loader';
import { VirtualGridWrapper } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper'; import { VirtualGridWrapper } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { Genre } from '/@/shared/types/domain/genre-domain-types'; import { AnyLibraryItem, Genre, LibraryItem } from '/@/shared/types/domain-types';
import { AnyLibraryItem, LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { ListDisplayType } from '/@/shared/types/types'; import { ListDisplayType } from '/@/shared/types/types';
export type VirtualInfiniteGridRef = { export type VirtualInfiniteGridRef = {
@@ -1,3 +1,4 @@
import type { AlbumArtist, Artist } from '/@/shared/types/domain-types';
import type { ICellRendererParams } from '@ag-grid-community/core'; import type { ICellRendererParams } from '@ag-grid-community/core';
import React from 'react'; import React from 'react';
@@ -9,7 +10,6 @@ import { AppRoute } from '/@/renderer/router/routes';
import { Separator } from '/@/shared/components/separator/separator'; import { Separator } from '/@/shared/components/separator/separator';
import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { Artist } from '/@/shared/types/domain/artist-domain-types';
export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => { export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
if (value === undefined) { if (value === undefined) {
@@ -23,7 +23,7 @@ export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
return ( return (
<CellContainer position="left"> <CellContainer position="left">
<Text isMuted overflow="hidden" size="md"> <Text isMuted overflow="hidden" size="md">
{value?.map((item: Artist, index: number) => ( {value?.map((item: AlbumArtist | Artist, index: number) => (
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}> <React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
{index > 0 && <Separator />} {index > 0 && <Separator />}
{item.id ? ( {item.id ? (
@@ -1,3 +1,4 @@
import type { AlbumArtist, Artist } from '/@/shared/types/domain-types';
import type { ICellRendererParams } from '@ag-grid-community/core'; import type { ICellRendererParams } from '@ag-grid-community/core';
import React from 'react'; import React from 'react';
@@ -9,7 +10,6 @@ import { AppRoute } from '/@/renderer/router/routes';
import { Separator } from '/@/shared/components/separator/separator'; import { Separator } from '/@/shared/components/separator/separator';
import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { Artist } from '/@/shared/types/domain/artist-domain-types';
export const ArtistCell = ({ data, value }: ICellRendererParams) => { export const ArtistCell = ({ data, value }: ICellRendererParams) => {
if (value === undefined) { if (value === undefined) {
@@ -23,7 +23,7 @@ export const ArtistCell = ({ data, value }: ICellRendererParams) => {
return ( return (
<CellContainer position="left"> <CellContainer position="left">
<Text isMuted overflow="hidden" size="md"> <Text isMuted overflow="hidden" size="md">
{value?.map((item: Artist, index: number) => ( {value?.map((item: AlbumArtist | Artist, index: number) => (
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}> <React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
{index > 0 && <Separator />} {index > 0 && <Separator />}
{item.id ? ( {item.id ? (
@@ -5,7 +5,7 @@ import styles from './combined-title-cell-controls.module.css';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
export const ListCoverControls = ({ export const ListCoverControls = ({
@@ -12,7 +12,7 @@ import { SEPARATOR_STRING } from '/@/shared/api/utils';
import { Image } from '/@/shared/components/image/image'; import { Image } from '/@/shared/components/image/image';
import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { Artist } from '/@/shared/types/domain/artist-domain-types'; import { AlbumArtist, Artist } from '/@/shared/types/domain-types';
export const CombinedTitleCell = ({ export const CombinedTitleCell = ({
context, context,
@@ -74,7 +74,7 @@ export const CombinedTitleCell = ({
</Text> </Text>
<Text isMuted overflow="hidden" size="md"> <Text isMuted overflow="hidden" size="md">
{artists?.length ? ( {artists?.length ? (
artists.map((artist: Artist, index: number) => ( artists.map((artist: AlbumArtist | Artist, index: number) => (
<React.Fragment key={`queue-${rowIndex}-artist-${artist.id}`}> <React.Fragment key={`queue-${rowIndex}-artist-${artist.id}`}>
{index > 0 ? SEPARATOR_STRING : null} {index > 0 ? SEPARATOR_STRING : null}
{artist.id ? ( {artist.id ? (
@@ -1,3 +1,4 @@
import type { AlbumArtist, Artist } from '/@/shared/types/domain-types';
import type { ICellRendererParams } from '@ag-grid-community/core'; import type { ICellRendererParams } from '@ag-grid-community/core';
import React from 'react'; import React from 'react';
@@ -7,14 +8,13 @@ import { CellContainer } from '/@/renderer/components/virtual-table/cells/generi
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route'; import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
import { Separator } from '/@/shared/components/separator/separator'; import { Separator } from '/@/shared/components/separator/separator';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { Artist } from '/@/shared/types/domain/artist-domain-types';
export const GenreCell = ({ data, value }: ICellRendererParams) => { export const GenreCell = ({ data, value }: ICellRendererParams) => {
const genrePath = useGenreRoute(); const genrePath = useGenreRoute();
return ( return (
<CellContainer position="left"> <CellContainer position="left">
<Text isMuted overflow="hidden" size="md"> <Text isMuted overflow="hidden" size="md">
{value?.map((item: Artist, index: number) => ( {value?.map((item: AlbumArtist | Artist, index: number) => (
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}> <React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
{index > 0 && <Separator />} {index > 0 && <Separator />}
<Text <Text
@@ -5,7 +5,7 @@ import { MutableRefObject, useEffect, useMemo, useRef } from 'react';
import { useAppFocus } from '/@/renderer/hooks'; import { useAppFocus } from '/@/renderer/hooks';
import { useCurrentSong, usePlayerStore } from '/@/renderer/store'; import { useCurrentSong, usePlayerStore } from '/@/renderer/store';
import { Song } from '/@/shared/types/domain/song-domain-types'; import { Song } from '/@/shared/types/domain-types';
import { PlayerStatus } from '/@/shared/types/types'; import { PlayerStatus } from '/@/shared/types/types';
interface UseCurrentSongRowStylesProps { interface UseCurrentSongRowStylesProps {
@@ -24,11 +24,11 @@ import { AppRoute } from '/@/renderer/router/routes';
import { PersistedTableColumn, useListStoreActions } from '/@/renderer/store'; import { PersistedTableColumn, useListStoreActions } from '/@/renderer/store';
import { ListKey, useListStoreByKey } from '/@/renderer/store/list.store'; import { ListKey, useListStoreByKey } from '/@/renderer/store/list.store';
import { import {
BasePaginatedQuery,
BasePaginatedResponse, BasePaginatedResponse,
} from '/@/shared/types/adapter/api-controller-types'; BaseQuery,
import { ServerListItem } from '/@/shared/types/domain/server-domain-types'; LibraryItem,
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; ServerListItem,
} from '/@/shared/types/domain-types';
import { ListDisplayType, TablePagination } from '/@/shared/types/types'; import { ListDisplayType, TablePagination } from '/@/shared/types/types';
export type AgGridFetchFn<TResponse, TFilter> = ( export type AgGridFetchFn<TResponse, TFilter> = (
@@ -52,7 +52,7 @@ interface UseAgGridProps<TFilter> {
const BLOCK_SIZE = 500; const BLOCK_SIZE = 500;
export const useVirtualTable = <TFilter extends BasePaginatedQuery<any>>({ export const useVirtualTable = <TFilter extends BaseQuery<any>>({
columnType, columnType,
contextMenu, contextMenu,
customFilters, customFilters,
@@ -18,7 +18,7 @@ import { Icon } from '/@/shared/components/icon/icon';
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area'; import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { ServerListItem, ServerType } from '/@/shared/types/domain/server-domain-types'; import { ServerListItem, ServerType } from '/@/shared/types/domain-types';
const localSettings = isElectron() ? window.api.localSettings : null; const localSettings = isElectron() ? window.api.localSettings : null;
@@ -1,45 +0,0 @@
import { queryOptions, UseQueryOptions } from '@tanstack/react-query';
import { api } from '/@/renderer/api/api-controller';
import { AlbumListRequest } from '/@/shared/types/domain/album-domain-types';
export const getAlbumListQueryKey = (serverId: string, request?: AlbumListRequest) => {
if (!request) {
return [serverId, 'albums'];
}
return [serverId, 'albums', request];
};
export const getInfiniteAlbumListQueryKey = (serverId: string, request?: AlbumListRequest) => {
if (!request) {
return [serverId, 'albums', 'infinite'];
}
return [serverId, 'albums', 'infinite', request];
};
export const getAlbumList = async (serverId: string, request: AlbumListRequest) => {
const [error, response] = await api.controller[serverId]!.album.getList!({
query: request.query,
});
if (error) {
throw new Error(error.message);
}
return response;
};
export const getAlbumListQuery = (
serverId: string,
request: AlbumListRequest,
options?: UseQueryOptions,
) => {
return queryOptions({
enabled: !!serverId,
queryFn: () => getAlbumList(serverId, request),
queryKey: getAlbumListQueryKey(serverId, request),
...options,
});
};
@@ -49,9 +49,13 @@ import { Group } from '/@/shared/components/group/group';
import { Popover } from '/@/shared/components/popover/popover'; import { Popover } from '/@/shared/components/popover/popover';
import { Spoiler } from '/@/shared/components/spoiler/spoiler'; import { Spoiler } from '/@/shared/components/spoiler/spoiler';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { AlbumListQuery, AlbumListSort } from '/@/shared/types/domain/album-domain-types'; import {
import { QueueSong } from '/@/shared/types/domain/player-domain-types'; AlbumListQuery,
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; AlbumListSort,
LibraryItem,
QueueSong,
SortOrder,
} from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
const isFullWidthRow = (node: RowNode) => { const isFullWidthRow = (node: RowNode) => {
@@ -150,8 +154,8 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
const artistQuery = useAlbumList({ const artistQuery = useAlbumList({
options: { options: {
cacheTime: 1000 * 60,
enabled: detailQuery?.data?.albumArtists[0]?.id !== undefined, enabled: detailQuery?.data?.albumArtists[0]?.id !== undefined,
gcTime: 1000 * 60,
keepPreviousData: true, keepPreviousData: true,
staleTime: 1000 * 60, staleTime: 1000 * 60,
}, },
@@ -165,9 +169,9 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
? [detailQuery?.data?.albumArtists[0].id] ? [detailQuery?.data?.albumArtists[0].id]
: undefined, : undefined,
limit: 15, limit: 15,
offset: 0,
sortBy: AlbumListSort.YEAR, sortBy: AlbumListSort.YEAR,
sortOrder: ListSortOrder.DESC, sortOrder: SortOrder.DESC,
startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); });
@@ -175,15 +179,15 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
const relatedAlbumGenresRequest: AlbumListQuery = { const relatedAlbumGenresRequest: AlbumListQuery = {
genres: detailQuery.data?.genres.length ? [detailQuery.data.genres[0].id] : undefined, genres: detailQuery.data?.genres.length ? [detailQuery.data.genres[0].id] : undefined,
limit: 15, limit: 15,
offset: 0,
sortBy: AlbumListSort.RANDOM, sortBy: AlbumListSort.RANDOM,
sortOrder: ListSortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: 0,
}; };
const relatedAlbumGenresQuery = useAlbumList({ const relatedAlbumGenresQuery = useAlbumList({
options: { options: {
cacheTime: 1000 * 60,
enabled: !!detailQuery?.data?.genres?.[0], enabled: !!detailQuery?.data?.genres?.[0],
gcTime: 1000 * 60,
queryKey: queryKeys.albums.related( queryKey: queryKeys.albums.related(
server?.id || '', server?.id || '',
albumId, albumId,
@@ -16,9 +16,7 @@ import { Group } from '/@/shared/components/group/group';
import { Rating } from '/@/shared/components/rating/rating'; import { Rating } from '/@/shared/components/rating/rating';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { AlbumDetailResponse } from '/@/shared/types/domain/album-domain-types'; import { AlbumDetailResponse, LibraryItem, ServerType } from '/@/shared/types/domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
interface AlbumDetailHeaderProps { interface AlbumDetailHeaderProps {
background: { background: {
@@ -21,8 +21,8 @@ import {
AlbumListQuery, AlbumListQuery,
AlbumListResponse, AlbumListResponse,
AlbumListSort, AlbumListSort,
} from '/@/shared/types/domain/album-domain-types'; LibraryItem,
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; } from '/@/shared/types/domain-types';
import { CardRow, ListDisplayType } from '/@/shared/types/types'; import { CardRow, ListDisplayType } from '/@/shared/types/types';
export const AlbumListGridView = ({ gridRef, itemCount }: any) => { export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
@@ -137,11 +137,15 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
const itemData: Album[] = []; const itemData: Album[] = [];
for (const [, data] of queriesFromCache) { for (const [, data] of queriesFromCache) {
const { items, offset } = data || {}; const { items, startIndex } = data || {};
if (items && items.length !== 1 && offset !== undefined) { if (items && items.length !== 1 && startIndex !== undefined) {
let itemIndex = 0; let itemIndex = 0;
for (let rowIndex = offset; rowIndex < offset + items.length; rowIndex += 1) { for (
let rowIndex = startIndex;
rowIndex < startIndex + items.length;
rowIndex += 1
) {
itemData[rowIndex] = items[itemIndex]; itemData[rowIndex] = items[itemIndex];
itemIndex += 1; itemIndex += 1;
} }
@@ -161,7 +165,7 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
limit: take, limit: take,
...filter, ...filter,
...customFilters, ...customFilters,
offset: skip, startIndex: skip,
}; };
const queryKey = queryKeys.albums.list(server?.id || '', query, id); const queryKey = queryKeys.albums.list(server?.id || '', query, id);
@@ -34,154 +34,158 @@ import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
import { Flex } from '/@/shared/components/flex/flex'; import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { AlbumListQuery, AlbumListSort } from '/@/shared/types/domain/album-domain-types'; import {
import { ServerType } from '/@/shared/types/domain/server-domain-types'; AlbumListQuery,
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; AlbumListSort,
LibraryItem,
ServerType,
SortOrder,
} from '/@/shared/types/domain-types';
import { ListDisplayType, Play, TableColumn } from '/@/shared/types/types'; import { ListDisplayType, Play, TableColumn } from '/@/shared/types/types';
const FILTERS = { const FILTERS = {
jellyfin: [ jellyfin: [
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }), name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
value: AlbumListSort.ALBUM_ARTIST, value: AlbumListSort.ALBUM_ARTIST,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.communityRating', { postProcess: 'titleCase' }), name: i18n.t('filter.communityRating', { postProcess: 'titleCase' }),
value: AlbumListSort.COMMUNITY_RATING, value: AlbumListSort.COMMUNITY_RATING,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.criticRating', { postProcess: 'titleCase' }), name: i18n.t('filter.criticRating', { postProcess: 'titleCase' }),
value: AlbumListSort.CRITIC_RATING, value: AlbumListSort.CRITIC_RATING,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }), name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumListSort.NAME, value: AlbumListSort.NAME,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.playCount', { postProcess: 'titleCase' }), name: i18n.t('filter.playCount', { postProcess: 'titleCase' }),
value: AlbumListSort.PLAY_COUNT, value: AlbumListSort.PLAY_COUNT,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }), name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: AlbumListSort.RANDOM, value: AlbumListSort.RANDOM,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }), name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_ADDED, value: AlbumListSort.RECENTLY_ADDED,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.releaseDate', { postProcess: 'titleCase' }), name: i18n.t('filter.releaseDate', { postProcess: 'titleCase' }),
value: AlbumListSort.RELEASE_DATE, value: AlbumListSort.RELEASE_DATE,
}, },
], ],
navidrome: [ navidrome: [
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }), name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
value: AlbumListSort.ALBUM_ARTIST, value: AlbumListSort.ALBUM_ARTIST,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.artist', { postProcess: 'titleCase' }), name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
value: AlbumListSort.ARTIST, value: AlbumListSort.ARTIST,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.duration', { postProcess: 'titleCase' }), name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
value: AlbumListSort.DURATION, value: AlbumListSort.DURATION,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }), name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.PLAY_COUNT, value: AlbumListSort.PLAY_COUNT,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }), name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumListSort.NAME, value: AlbumListSort.NAME,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }), name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: AlbumListSort.RANDOM, value: AlbumListSort.RANDOM,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.rating', { postProcess: 'titleCase' }), name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
value: AlbumListSort.RATING, value: AlbumListSort.RATING,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }), name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_ADDED, value: AlbumListSort.RECENTLY_ADDED,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }), name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_PLAYED, value: AlbumListSort.RECENTLY_PLAYED,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }), name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
value: AlbumListSort.SONG_COUNT, value: AlbumListSort.SONG_COUNT,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.favorited', { postProcess: 'titleCase' }), name: i18n.t('filter.favorited', { postProcess: 'titleCase' }),
value: AlbumListSort.FAVORITED, value: AlbumListSort.FAVORITED,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }), name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
value: AlbumListSort.YEAR, value: AlbumListSort.YEAR,
}, },
], ],
subsonic: [ subsonic: [
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }), name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
value: AlbumListSort.ALBUM_ARTIST, value: AlbumListSort.ALBUM_ARTIST,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }), name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.PLAY_COUNT, value: AlbumListSort.PLAY_COUNT,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }), name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumListSort.NAME, value: AlbumListSort.NAME,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }), name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: AlbumListSort.RANDOM, value: AlbumListSort.RANDOM,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }), name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_ADDED, value: AlbumListSort.RECENTLY_ADDED,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }), name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_PLAYED, value: AlbumListSort.RECENTLY_PLAYED,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.favorited', { postProcess: 'titleCase' }), name: i18n.t('filter.favorited', { postProcess: 'titleCase' }),
value: AlbumListSort.FAVORITED, value: AlbumListSort.FAVORITED,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }), name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
value: AlbumListSort.YEAR, value: AlbumListSort.YEAR,
}, },
@@ -295,7 +299,7 @@ export const AlbumListHeaderFilters = ({
customFilters, customFilters,
data: { data: {
sortBy: e.currentTarget.value as AlbumListSort, sortBy: e.currentTarget.value as AlbumListSort,
sortOrder: sortOrder || ListSortOrder.ASC, sortOrder: sortOrder || SortOrder.ASC,
}, },
itemType: LibraryItem.ALBUM, itemType: LibraryItem.ALBUM,
key: pageKey, key: pageKey,
@@ -333,8 +337,7 @@ export const AlbumListHeaderFilters = ({
); );
const handleToggleSortOrder = useCallback(() => { const handleToggleSortOrder = useCallback(() => {
const newSortOrder = const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
filter.sortOrder === ListSortOrder.ASC ? ListSortOrder.DESC : ListSortOrder.ASC;
const updatedFilters = setFilter({ const updatedFilters = setFilter({
customFilters, customFilters,
data: { sortOrder: newSortOrder }, data: { sortOrder: newSortOrder },
@@ -16,8 +16,7 @@ import { titleCase } from '/@/renderer/utils';
import { Flex } from '/@/shared/components/flex/flex'; import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { AlbumListQuery } from '/@/shared/types/domain/album-domain-types'; import { AlbumListQuery, LibraryItem } from '/@/shared/types/domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
interface AlbumListHeaderProps { interface AlbumListHeaderProps {
genreId?: string; genreId?: string;
@@ -4,7 +4,7 @@ import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-
import { useListContext } from '/@/renderer/context/list-context'; import { useListContext } from '/@/renderer/context/list-context';
import { ALBUM_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; import { ALBUM_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
export const AlbumListTableView = ({ itemCount, tableRef }: any) => { export const AlbumListTableView = ({ itemCount, tableRef }: any) => {
const server = useCurrentServer(); const server = useCurrentServer();
@@ -14,10 +14,13 @@ import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select'; import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
import { AlbumListQuery } from '/@/shared/types/domain/album-domain-types'; import {
import { AlbumArtistListSort } from '/@/shared/types/domain/artist-domain-types'; AlbumArtistListSort,
import { GenreListSort } from '/@/shared/types/domain/genre-domain-types'; AlbumListQuery,
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; GenreListSort,
LibraryItem,
SortOrder,
} from '/@/shared/types/domain-types';
interface JellyfinAlbumFiltersProps { interface JellyfinAlbumFiltersProps {
customFilters?: Partial<AlbumListFilter>; customFilters?: Partial<AlbumListFilter>;
@@ -41,14 +44,14 @@ export const JellyfinAlbumFilters = ({
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library // TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
const genreListQuery = useGenreList({ const genreListQuery = useGenreList({
options: { options: {
gcTime: 1000 * 60 * 2, cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
musicFolderId: filter?.musicFolderId, musicFolderId: filter?.musicFolderId,
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC, sortOrder: SortOrder.ASC,
offset: 0, startIndex: 0,
}, },
serverId, serverId,
}); });
@@ -63,7 +66,7 @@ export const JellyfinAlbumFilters = ({
const tagsQuery = useTagList({ const tagsQuery = useTagList({
options: { options: {
gcTime: 1000 * 60 * 2, cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
@@ -170,12 +173,12 @@ export const JellyfinAlbumFilters = ({
const albumArtistListQuery = useAlbumArtistList({ const albumArtistListQuery = useAlbumArtistList({
options: { options: {
gcTime: 1000 * 60 * 2, cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
sortBy: AlbumArtistListSort.NAME, sortBy: AlbumArtistListSort.NAME,
sortOrder: ListSortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: 0, startIndex: 0,
}, },
serverId, serverId,
@@ -2,12 +2,21 @@ import debounce from 'lodash/debounce';
import { ChangeEvent, useMemo } from 'react'; import { ChangeEvent, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data'; import {
MultiSelectWithInvalidData,
SelectWithInvalidData,
} from '/@/renderer/components/select-with-invalid-data';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query'; import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
import { useGenreList } from '/@/renderer/features/genres'; import { useGenreList } from '/@/renderer/features/genres';
import { useTagList } from '/@/renderer/features/tag/queries/use-tag-list'; import { useTagList } from '/@/renderer/features/tag/queries/use-tag-list';
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store'; import {
AlbumListFilter,
getServerById,
useListStoreActions,
useListStoreByKey,
} from '/@/renderer/store';
import { NDSongQueryFields } from '/@/shared/api/navidrome.types'; import { NDSongQueryFields } from '/@/shared/api/navidrome.types';
import { hasFeature } from '/@/shared/api/utils';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { NumberInput } from '/@/shared/components/number-input/number-input'; import { NumberInput } from '/@/shared/components/number-input/number-input';
@@ -16,10 +25,14 @@ import { Stack } from '/@/shared/components/stack/stack';
import { Switch } from '/@/shared/components/switch/switch'; import { Switch } from '/@/shared/components/switch/switch';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select'; import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
import { AlbumListQuery } from '/@/shared/types/domain/album-domain-types'; import {
import { AlbumArtistListSort } from '/@/shared/types/domain/artist-domain-types'; AlbumArtistListSort,
import { GenreListSort } from '/@/shared/types/domain/genre-domain-types'; AlbumListQuery,
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; GenreListSort,
LibraryItem,
SortOrder,
} from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
interface NavidromeAlbumFiltersProps { interface NavidromeAlbumFiltersProps {
customFilters?: Partial<AlbumListFilter>; customFilters?: Partial<AlbumListFilter>;
@@ -39,16 +52,17 @@ export const NavidromeAlbumFilters = ({
const { t } = useTranslation(); const { t } = useTranslation();
const { filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey }); const { filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey });
const { setFilter } = useListStoreActions(); const { setFilter } = useListStoreActions();
const server = getServerById(serverId);
const genreListQuery = useGenreList({ const genreListQuery = useGenreList({
options: { options: {
gcTime: 1000 * 60 * 2, cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC, sortOrder: SortOrder.ASC,
offset: 0, startIndex: 0,
}, },
serverId, serverId,
}); });
@@ -61,12 +75,14 @@ export const NavidromeAlbumFilters = ({
})); }));
}, [genreListQuery.data]); }, [genreListQuery.data]);
const handleGenresFilter = debounce((e: null | string) => { const hasBrf = hasFeature(server, ServerFeature.BFR);
const handleGenresFilter = debounce((e: null | string[]) => {
const updatedFilters = setFilter({ const updatedFilters = setFilter({
customFilters, customFilters,
data: { data: {
_custom: filter._custom, _custom: filter._custom,
genres: e ? [e] : undefined, genres: e ? e : undefined,
}, },
itemType: LibraryItem.ALBUM, itemType: LibraryItem.ALBUM,
key: pageKey, key: pageKey,
@@ -76,7 +92,7 @@ export const NavidromeAlbumFilters = ({
const tagsQuery = useTagList({ const tagsQuery = useTagList({
options: { options: {
gcTime: 1000 * 60 * 2, cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
@@ -185,13 +201,13 @@ export const NavidromeAlbumFilters = ({
const albumArtistListQuery = useAlbumArtistList({ const albumArtistListQuery = useAlbumArtistList({
options: { options: {
gcTime: 1000 * 60 * 2, cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
// searchTerm: debouncedSearchTerm, // searchTerm: debouncedSearchTerm,
sortBy: AlbumArtistListSort.NAME, sortBy: AlbumArtistListSort.NAME,
sortOrder: ListSortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: 0, startIndex: 0,
}, },
serverId, serverId,
@@ -266,15 +282,29 @@ export const NavidromeAlbumFilters = ({
min={0} min={0}
onChange={(e) => handleYearFilter(e)} onChange={(e) => handleYearFilter(e)}
/> />
{!hasBrf && (
<SelectWithInvalidData <SelectWithInvalidData
clearable clearable
data={genreList} data={genreList}
defaultValue={filter.genres && filter.genres[0]} defaultValue={filter.genres && filter.genres[0]}
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })} label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
onChange={(value) => handleGenresFilter(value !== null ? [value] : null)}
searchable
/>
)}
</Group>
{hasBrf && (
<Group grow>
<MultiSelectWithInvalidData
clearable
data={genreList}
defaultValue={filter.genres}
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
onChange={handleGenresFilter} onChange={handleGenresFilter}
searchable searchable
/> />
</Group> </Group>
)}
<Group grow> <Group grow>
<SelectWithInvalidData <SelectWithInvalidData
clearable clearable
@@ -14,10 +14,13 @@ import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Switch } from '/@/shared/components/switch/switch'; import { Switch } from '/@/shared/components/switch/switch';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { AlbumListQuery } from '/@/shared/types/domain/album-domain-types'; import {
import { AlbumArtistListSort } from '/@/shared/types/domain/artist-domain-types'; AlbumArtistListSort,
import { GenreListSort } from '/@/shared/types/domain/genre-domain-types'; AlbumListQuery,
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; GenreListSort,
LibraryItem,
SortOrder,
} from '/@/shared/types/domain-types';
interface SubsonicAlbumFiltersProps { interface SubsonicAlbumFiltersProps {
disableArtistFilter?: boolean; disableArtistFilter?: boolean;
@@ -39,12 +42,12 @@ export const SubsonicAlbumFilters = ({
const albumArtistListQuery = useAlbumArtistList({ const albumArtistListQuery = useAlbumArtistList({
options: { options: {
gcTime: 1000 * 60 * 2, cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
sortBy: AlbumArtistListSort.NAME, sortBy: AlbumArtistListSort.NAME,
sortOrder: ListSortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: 0, startIndex: 0,
}, },
serverId, serverId,
@@ -72,13 +75,13 @@ export const SubsonicAlbumFilters = ({
const genreListQuery = useGenreList({ const genreListQuery = useGenreList({
options: { options: {
gcTime: 1000 * 60 * 2, cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1, staleTime: 1000 * 60 * 1,
}, },
query: { query: {
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC, sortOrder: SortOrder.ASC,
offset: 0, startIndex: 0,
}, },
serverId, serverId,
}); });
@@ -1,15 +1,15 @@
import type { RQueryHookArgs } from '/@/renderer/lib/react-query'; import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import type { AlbumDetailQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { controller } from '/@/renderer/api/controller'; import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { useServerById } from '/@/renderer/store'; import { getServerById } from '/@/renderer/store';
import { AlbumDetailQuery } from '/@/shared/types/domain/album-domain-types';
export const useAlbumDetail = (args: RQueryHookArgs<AlbumDetailQuery>) => { export const useAlbumDetail = (args: QueryHookArgs<AlbumDetailQuery>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = getServerById(serverId);
return useQuery({ return useQuery({
queryFn: ({ signal }) => { queryFn: ({ signal }) => {
@@ -1,15 +1,15 @@
import type { RQueryHookArgs } from '/@/renderer/lib/react-query'; import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import type { AlbumListQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { useServerById } from '/@/renderer/store'; import { getServerById } from '/@/renderer/store';
import { AlbumListQuery } from '/@/shared/types/domain/album-domain-types';
export const useAlbumListCount = (args: RQueryHookArgs<AlbumListQuery>) => { export const useAlbumListCount = (args: QueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = getServerById(serverId);
return useQuery({ return useQuery({
enabled: !!serverId, enabled: !!serverId,
@@ -1,16 +1,16 @@
import type { RQueryHookArgs } from '/@/renderer/lib/react-query'; import type { QueryHookArgs } from '/@/renderer/lib/react-query';
import type { AlbumListQuery, AlbumListResponse } from '/@/shared/types/domain-types';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { controller } from '/@/renderer/api/controller'; import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { useServerById } from '/@/renderer/store'; import { getServerById } from '/@/renderer/store';
import { AlbumListQuery, AlbumListResponse } from '/@/shared/types/domain/album-domain-types';
export const useAlbumList = (args: RQueryHookArgs<AlbumListQuery>) => { export const useAlbumList = (args: QueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = getServerById(serverId);
return useQuery({ return useQuery({
enabled: !!serverId, enabled: !!serverId,
@@ -33,9 +33,9 @@ export const useAlbumList = (args: RQueryHookArgs<AlbumListQuery>) => {
}); });
}; };
export const useAlbumListInfinite = (args: RQueryHookArgs<AlbumListQuery>) => { export const useAlbumListInfinite = (args: QueryHookArgs<AlbumListQuery>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = getServerById(serverId);
return useInfiniteQuery({ return useInfiniteQuery({
enabled: !!serverId, enabled: !!serverId,
@@ -57,7 +57,7 @@ export const useAlbumListInfinite = (args: RQueryHookArgs<AlbumListQuery>) => {
query: { query: {
...query, ...query,
limit: query.limit || 50, limit: query.limit || 50,
offset: pageParam * (query.limit || 50), startIndex: pageParam * (query.limit || 50),
}, },
}); });
}, },
@@ -12,7 +12,7 @@ import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared';
import { useFastAverageColor } from '/@/renderer/hooks'; import { useFastAverageColor } from '/@/renderer/hooks';
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store'; import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
const AlbumDetailRoute = () => { const AlbumDetailRoute = () => {
const tableRef = useRef<AgGridReactType | null>(null); const tableRef = useRef<AgGridReactType | null>(null);
@@ -16,9 +16,12 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
import { AnimatedPage } from '/@/renderer/features/shared'; import { AnimatedPage } from '/@/renderer/features/shared';
import { queryClient } from '/@/renderer/lib/react-query'; import { queryClient } from '/@/renderer/lib/react-query';
import { useCurrentServer, useListFilterByKey } from '/@/renderer/store'; import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
import { AlbumListQuery } from '/@/shared/types/domain/album-domain-types'; import {
import { GenreListSort } from '/@/shared/types/domain/genre-domain-types'; AlbumListQuery,
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; GenreListSort,
LibraryItem,
SortOrder,
} from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
const AlbumListRoute = () => { const AlbumListRoute = () => {
@@ -52,13 +55,13 @@ const AlbumListRoute = () => {
const genreList = useGenreList({ const genreList = useGenreList({
options: { options: {
gcTime: 1000 * 60 * 60, cacheTime: 1000 * 60 * 60,
enabled: !!genreId, enabled: !!genreId,
}, },
query: { query: {
sortBy: GenreListSort.NAME, sortBy: GenreListSort.NAME,
sortOrder: ListSortOrder.ASC, sortOrder: SortOrder.ASC,
offset: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); });
@@ -74,7 +77,7 @@ const AlbumListRoute = () => {
const itemCountCheck = useAlbumListCount({ const itemCountCheck = useAlbumListCount({
options: { options: {
gcTime: 1000 * 60, cacheTime: 1000 * 60,
staleTime: 1000 * 60, staleTime: 1000 * 60,
}, },
query: { query: {
@@ -33,8 +33,7 @@ import { Icon } from '/@/shared/components/icon/icon';
import { Spoiler } from '/@/shared/components/spoiler/spoiler'; import { Spoiler } from '/@/shared/components/spoiler/spoiler';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem, SongDetailResponse } from '/@/shared/types/domain-types';
import { SongDetailResponse } from '/@/shared/types/domain/song-domain-types';
const DummyAlbumDetailRoute = () => { const DummyAlbumDetailRoute = () => {
const cq = useContainerQuery(); const cq = useContainerQuery();
@@ -9,8 +9,4 @@
gap: var(--theme-spacing-lg); gap: var(--theme-spacing-lg);
padding: 1rem 2rem 5rem; padding: 1rem 2rem 5rem;
overflow: hidden; overflow: hidden;
:global(.ag-theme-alpine-dark) {
--ag-header-background-color: rgb(0 0 0 / 0%) !important;
}
} }
@@ -35,10 +35,15 @@ import { Group } from '/@/shared/components/group/group';
import { Spoiler } from '/@/shared/components/spoiler/spoiler'; import { Spoiler } from '/@/shared/components/spoiler/spoiler';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { TextTitle } from '/@/shared/components/text-title/text-title'; import { TextTitle } from '/@/shared/components/text-title/text-title';
import { Album, AlbumListSort } from '/@/shared/types/domain/album-domain-types'; import {
import { QueueSong } from '/@/shared/types/domain/player-domain-types'; Album,
import { ServerType } from '/@/shared/types/domain/server-domain-types'; AlbumArtist,
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; AlbumListSort,
LibraryItem,
QueueSong,
ServerType,
SortOrder,
} from '/@/shared/types/domain-types';
import { CardRow, Play, TableColumn } from '/@/shared/types/types'; import { CardRow, Play, TableColumn } from '/@/shared/types/types';
interface AlbumArtistDetailContentProps { interface AlbumArtistDetailContentProps {
@@ -101,8 +106,8 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
compilation: false, compilation: false,
limit: 15, limit: 15,
sortBy: AlbumListSort.RELEASE_DATE, sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: ListSortOrder.DESC, sortOrder: SortOrder.DESC,
offset: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); });
@@ -116,8 +121,8 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
compilation: true, compilation: true,
limit: 15, limit: 15,
sortBy: AlbumListSort.RELEASE_DATE, sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: ListSortOrder.DESC, sortOrder: SortOrder.DESC,
offset: 0, startIndex: 0,
}, },
serverId: server?.id, serverId: server?.id,
}); });
@@ -11,16 +11,18 @@ import { Group } from '/@/shared/components/group/group';
import { Rating } from '/@/shared/components/rating/rating'; import { Rating } from '/@/shared/components/rating/rating';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { ServerType } from '/@/shared/types/domain/server-domain-types'; import { LibraryItem, ServerType } from '/@/shared/types/domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
interface AlbumArtistDetailHeaderProps { interface AlbumArtistDetailHeaderProps {
background: {
background?: string; background?: string;
blur: number;
loading: boolean; loading: boolean;
};
} }
export const AlbumArtistDetailHeader = forwardRef( export const AlbumArtistDetailHeader = forwardRef(
({ background, loading }: AlbumArtistDetailHeaderProps, ref: Ref<HTMLDivElement>) => { ({ background }: AlbumArtistDetailHeaderProps, ref: Ref<HTMLDivElement>) => {
const { albumArtistId, artistId } = useParams() as { const { albumArtistId, artistId } = useParams() as {
albumArtistId?: string; albumArtistId?: string;
artistId?: string; artistId?: string;
@@ -77,12 +79,11 @@ export const AlbumArtistDetailHeader = forwardRef(
return ( return (
<LibraryHeader <LibraryHeader
background={background}
imageUrl={detailQuery?.data?.imageUrl} imageUrl={detailQuery?.data?.imageUrl}
item={{ route: AppRoute.LIBRARY_ALBUM_ARTISTS, type: LibraryItem.ALBUM_ARTIST }} item={{ route: AppRoute.LIBRARY_ALBUM_ARTISTS, type: LibraryItem.ALBUM_ARTIST }}
loading={loading}
ref={ref} ref={ref}
title={detailQuery?.data?.name || ''} title={detailQuery?.data?.name || ''}
{...background}
> >
<Stack> <Stack>
<Group> <Group>
@@ -12,9 +12,7 @@ import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/conte
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { QueueSong } from '/@/shared/types/domain/player-domain-types'; import { LibraryItem, QueueSong, SongListQuery } from '/@/shared/types/domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { SongListQuery } from '/@/shared/types/domain/song-domain-types';
interface AlbumArtistSongListContentProps { interface AlbumArtistSongListContentProps {
data: QueueSong[]; data: QueueSong[];
@@ -6,7 +6,7 @@ import { LibraryHeaderBar } from '/@/renderer/features/shared';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { Badge } from '/@/shared/components/badge/badge'; import { Badge } from '/@/shared/components/badge/badge';
import { SpinnerIcon } from '/@/shared/components/spinner/spinner'; import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
import { QueueSong } from '/@/shared/types/domain/player-domain-types'; import { QueueSong } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
interface AlbumArtistDetailTopSongsListHeaderProps { interface AlbumArtistDetailTopSongsListHeaderProps {
@@ -17,11 +17,12 @@ import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store'; import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
import { import {
AlbumArtist,
AlbumArtistListQuery, AlbumArtistListQuery,
AlbumArtistListResponse, AlbumArtistListResponse,
AlbumArtistListSort, AlbumArtistListSort,
} from '/@/shared/types/domain/artist-domain-types'; LibraryItem,
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; } from '/@/shared/types/domain-types';
import { CardRow, ListDisplayType } from '/@/shared/types/types'; import { CardRow, ListDisplayType } from '/@/shared/types/types';
interface AlbumArtistListGridViewProps { interface AlbumArtistListGridViewProps {
@@ -34,90 +34,91 @@ import { Icon } from '/@/shared/components/icon/icon';
import { import {
AlbumArtistListQuery, AlbumArtistListQuery,
AlbumArtistListSort, AlbumArtistListSort,
} from '/@/shared/types/domain/artist-domain-types'; LibraryItem,
import { ServerType } from '/@/shared/types/domain/server-domain-types'; ServerType,
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; SortOrder,
} from '/@/shared/types/domain-types';
import { ListDisplayType } from '/@/shared/types/types'; import { ListDisplayType } from '/@/shared/types/types';
const FILTERS = { const FILTERS = {
jellyfin: [ jellyfin: [
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.album', { postProcess: 'titleCase' }), name: i18n.t('filter.album', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.ALBUM, value: AlbumArtistListSort.ALBUM,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.duration', { postProcess: 'titleCase' }), name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.DURATION, value: AlbumArtistListSort.DURATION,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }), name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.NAME, value: AlbumArtistListSort.NAME,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }), name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.RANDOM, value: AlbumArtistListSort.RANDOM,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }), name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.RECENTLY_ADDED, value: AlbumArtistListSort.RECENTLY_ADDED,
}, },
// { defaultOrder: ListSortOrder.DESC, name: 'Release Date', value: AlbumArtistListSort.RELEASE_DATE }, // { defaultOrder: SortOrder.DESC, name: 'Release Date', value: AlbumArtistListSort.RELEASE_DATE },
], ],
navidrome: [ navidrome: [
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }), name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.ALBUM_COUNT, value: AlbumArtistListSort.ALBUM_COUNT,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }), name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.FAVORITED, value: AlbumArtistListSort.FAVORITED,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }), name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.PLAY_COUNT, value: AlbumArtistListSort.PLAY_COUNT,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }), name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.NAME, value: AlbumArtistListSort.NAME,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.rating', { postProcess: 'titleCase' }), name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.RATING, value: AlbumArtistListSort.RATING,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }), name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.SONG_COUNT, value: AlbumArtistListSort.SONG_COUNT,
}, },
], ],
subsonic: [ subsonic: [
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }), name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.ALBUM_COUNT, value: AlbumArtistListSort.ALBUM_COUNT,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }), name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.FAVORITED, value: AlbumArtistListSort.FAVORITED,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }), name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.NAME, value: AlbumArtistListSort.NAME,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.rating', { postProcess: 'titleCase' }), name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
value: AlbumArtistListSort.RATING, value: AlbumArtistListSort.RATING,
}, },
@@ -269,7 +270,7 @@ export const AlbumArtistListHeaderFilters = ({
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
sortBy: e.currentTarget.value as AlbumArtistListSort, sortBy: e.currentTarget.value as AlbumArtistListSort,
sortOrder: sortOrder || ListSortOrder.ASC, sortOrder: sortOrder || SortOrder.ASC,
}, },
itemType: LibraryItem.ALBUM_ARTIST, itemType: LibraryItem.ALBUM_ARTIST,
key: pageKey, key: pageKey,
@@ -305,8 +306,7 @@ export const AlbumArtistListHeaderFilters = ({
); );
const handleToggleSortOrder = useCallback(() => { const handleToggleSortOrder = useCallback(() => {
const newSortOrder = const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
filter.sortOrder === ListSortOrder.ASC ? ListSortOrder.DESC : ListSortOrder.ASC;
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { sortOrder: newSortOrder }, data: { sortOrder: newSortOrder },
itemType: LibraryItem.ALBUM_ARTIST, itemType: LibraryItem.ALBUM_ARTIST,
@@ -15,8 +15,7 @@ import { AlbumArtistListFilter, useCurrentServer } from '/@/renderer/store';
import { Flex } from '/@/shared/components/flex/flex'; import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { AlbumArtistListQuery } from '/@/shared/types/domain/artist-domain-types'; import { AlbumArtistListQuery, LibraryItem } from '/@/shared/types/domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
interface AlbumArtistListHeaderProps { interface AlbumArtistListHeaderProps {
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>; gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
@@ -8,7 +8,7 @@ import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-
import { useListContext } from '/@/renderer/context/list-context'; import { useListContext } from '/@/renderer/context/list-context';
import { ARTIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; import { ARTIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
interface AlbumArtistListTableViewProps { interface AlbumArtistListTableViewProps {
itemCount?: number; itemCount?: number;
@@ -18,11 +18,12 @@ import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useListStoreActions } from '/@/renderer/store'; import { useCurrentServer, useListStoreActions } from '/@/renderer/store';
import { useListStoreByKey } from '/@/renderer/store/list.store'; import { useListStoreByKey } from '/@/renderer/store/list.store';
import { import {
AlbumArtist,
ArtistListQuery, ArtistListQuery,
ArtistListResponse, ArtistListResponse,
ArtistListSort, ArtistListSort,
} from '/@/shared/types/domain/artist-domain-types'; LibraryItem,
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; } from '/@/shared/types/domain-types';
import { CardRow, ListDisplayType } from '/@/shared/types/types'; import { CardRow, ListDisplayType } from '/@/shared/types/types';
interface ArtistListGridViewProps { interface ArtistListGridViewProps {
@@ -55,11 +56,15 @@ export const ArtistListGridView = ({ gridRef, itemCount }: ArtistListGridViewPro
const itemData: AlbumArtist[] = []; const itemData: AlbumArtist[] = [];
for (const [, data] of queriesFromCache) { for (const [, data] of queriesFromCache) {
const { items, offset } = data || {}; const { items, startIndex } = data || {};
if (items && items.length !== 1 && offset !== undefined) { if (items && items.length !== 1 && startIndex !== undefined) {
let itemIndex = 0; let itemIndex = 0;
for (let rowIndex = offset; rowIndex < offset + items.length; rowIndex += 1) { for (
let rowIndex = startIndex;
rowIndex < startIndex + items.length;
rowIndex += 1
) {
itemData[rowIndex] = items[itemIndex]; itemData[rowIndex] = items[itemIndex];
itemIndex += 1; itemIndex += 1;
} }
@@ -74,7 +79,7 @@ export const ArtistListGridView = ({ gridRef, itemCount }: ArtistListGridViewPro
const query: ArtistListQuery = { const query: ArtistListQuery = {
...filter, ...filter,
limit, limit,
offset, startIndex,
}; };
const queryKey = queryKeys.artists.list(server?.id || '', query); const queryKey = queryKeys.artists.list(server?.id || '', query);
@@ -33,89 +33,93 @@ import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { Select } from '/@/shared/components/select/select'; import { Select } from '/@/shared/components/select/select';
import { ArtistListQuery, ArtistListSort } from '/@/shared/types/domain/artist-domain-types'; import {
import { ServerType } from '/@/shared/types/domain/server-domain-types'; ArtistListQuery,
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; ArtistListSort,
LibraryItem,
ServerType,
SortOrder,
} from '/@/shared/types/domain-types';
import { ListDisplayType } from '/@/shared/types/types'; import { ListDisplayType } from '/@/shared/types/types';
const FILTERS = { const FILTERS = {
jellyfin: [ jellyfin: [
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.album', { postProcess: 'titleCase' }), name: i18n.t('filter.album', { postProcess: 'titleCase' }),
value: ArtistListSort.ALBUM, value: ArtistListSort.ALBUM,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.duration', { postProcess: 'titleCase' }), name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
value: ArtistListSort.DURATION, value: ArtistListSort.DURATION,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }), name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: ArtistListSort.NAME, value: ArtistListSort.NAME,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }), name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: ArtistListSort.RANDOM, value: ArtistListSort.RANDOM,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }), name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: ArtistListSort.RECENTLY_ADDED, value: ArtistListSort.RECENTLY_ADDED,
}, },
], ],
navidrome: [ navidrome: [
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }), name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
value: ArtistListSort.ALBUM_COUNT, value: ArtistListSort.ALBUM_COUNT,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }), name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
value: ArtistListSort.FAVORITED, value: ArtistListSort.FAVORITED,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }), name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
value: ArtistListSort.PLAY_COUNT, value: ArtistListSort.PLAY_COUNT,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }), name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: ArtistListSort.NAME, value: ArtistListSort.NAME,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.rating', { postProcess: 'titleCase' }), name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
value: ArtistListSort.RATING, value: ArtistListSort.RATING,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }), name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
value: ArtistListSort.SONG_COUNT, value: ArtistListSort.SONG_COUNT,
}, },
], ],
subsonic: [ subsonic: [
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }), name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
value: ArtistListSort.ALBUM_COUNT, value: ArtistListSort.ALBUM_COUNT,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }), name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
value: ArtistListSort.FAVORITED, value: ArtistListSort.FAVORITED,
}, },
{ {
defaultOrder: ListSortOrder.ASC, defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }), name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: ArtistListSort.NAME, value: ArtistListSort.NAME,
}, },
{ {
defaultOrder: ListSortOrder.DESC, defaultOrder: SortOrder.DESC,
name: i18n.t('filter.rating', { postProcess: 'titleCase' }), name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
value: ArtistListSort.RATING, value: ArtistListSort.RATING,
}, },
@@ -140,7 +144,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
const cq = useContainerQuery(); const cq = useContainerQuery();
const roles = useRoles({ const roles = useRoles({
options: { options: {
gcTime: 1000 * 60 * 60 * 2, cacheTime: 1000 * 60 * 60 * 2,
staleTime: 1000 * 60 * 60 * 2, staleTime: 1000 * 60 * 60 * 2,
}, },
query: {}, query: {},
@@ -188,7 +192,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
}, },
query: { query: {
limit, limit,
offset, startIndex,
...filters, ...filters,
}, },
}), }),
@@ -224,7 +228,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
}, },
query: { query: {
limit, limit,
offset, startIndex,
...filters, ...filters,
}, },
}), }),
@@ -272,7 +276,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
sortBy: e.currentTarget.value as ArtistListSort, sortBy: e.currentTarget.value as ArtistListSort,
sortOrder: sortOrder || ListSortOrder.ASC, sortOrder: sortOrder || SortOrder.ASC,
}, },
itemType: LibraryItem.ARTIST, itemType: LibraryItem.ARTIST,
key: pageKey, key: pageKey,
@@ -308,8 +312,7 @@ export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderF
); );
const handleToggleSortOrder = useCallback(() => { const handleToggleSortOrder = useCallback(() => {
const newSortOrder = const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
filter.sortOrder === ListSortOrder.ASC ? ListSortOrder.DESC : ListSortOrder.ASC;
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { sortOrder: newSortOrder }, data: { sortOrder: newSortOrder },
itemType: LibraryItem.ARTIST, itemType: LibraryItem.ARTIST,
@@ -15,8 +15,7 @@ import { ArtistListFilter, useCurrentServer } from '/@/renderer/store';
import { Flex } from '/@/shared/components/flex/flex'; import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { ArtistListQuery } from '/@/shared/types/domain/artist-domain-types'; import { ArtistListQuery, LibraryItem } from '/@/shared/types/domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
interface ArtistListHeaderProps { interface ArtistListHeaderProps {
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>; gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
@@ -8,7 +8,7 @@ import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-
import { useListContext } from '/@/renderer/context/list-context'; import { useListContext } from '/@/renderer/context/list-context';
import { ARTIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; import { ARTIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
interface ArtistListTableViewProps { interface ArtistListTableViewProps {
itemCount?: number; itemCount?: number;
@@ -1,14 +1,15 @@
import type { AlbumArtistDetailQuery } from '/@/shared/types/domain-types';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { RQueryHookArgs } from '/@/renderer/lib/react-query'; import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { getServerById } from '/@/renderer/store';
import { AlbumArtistDetailQuery } from '/@/shared/types/domain/artist-domain-types';
export const useAlbumArtistDetail = (args: RQueryHookArgs<AlbumArtistDetailQuery>) => { export const useAlbumArtistDetail = (args: QueryHookArgs<AlbumArtistDetailQuery>) => {
const { options, query, serverId } = args || {}; const { options, query, serverId } = args || {};
const server = useServerById(serverId); const server = getServerById(serverId);
return useQuery({ return useQuery({
enabled: !!server?.id && !!query.id, enabled: !!server?.id && !!query.id,
@@ -2,13 +2,13 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { RQueryHookArgs } from '/@/renderer/lib/react-query'; import { QueryHookArgs } from '/@/renderer/lib/react-query';
import { useServerById } from '/@/renderer/store'; import { getServerById } from '/@/renderer/store';
import { AlbumArtistListQuery } from '/@/shared/types/domain/artist-domain-types'; import { AlbumArtistListQuery } from '/@/shared/types/domain-types';
export const useAlbumArtistListCount = (args: RQueryHookArgs<AlbumArtistListQuery>) => { export const useAlbumArtistListCount = (args: QueryHookArgs<AlbumArtistListQuery>) => {
const { options, query, serverId } = args; const { options, query, serverId } = args;
const server = useServerById(serverId); const server = getServerById(serverId);
return useQuery({ return useQuery({
enabled: !!serverId, enabled: !!serverId,

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