From 01d26cf02df6d56e0bc740b151d7a522e9d7218a Mon Sep 17 00:00:00 2001 From: Sean Parsons Date: Wed, 25 Mar 2026 21:42:47 +0000 Subject: [PATCH] Add GHCR Docker workflow for new-multithreaded-import; multithread scan changes - Dockerfile.multithread: build from repo root for CI overlay on linuxserver/lidarr:nightly - docker-ghcr-multithread.yml: push to ghcr.io on branch push + workflow_dispatch - MediaFiles / RefreshArtist: fork behavior for multithreaded import Made-with: Cursor --- .github/workflows/docker-ghcr-multithread.yml | 43 +++++++++++++++++++ Dockerfile.multithread | 19 ++++++++ .../MediaFiles/DiskScanService.cs | 31 +++++++------ .../Music/Services/RefreshArtistService.cs | 7 +++ 4 files changed, 86 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/docker-ghcr-multithread.yml create mode 100644 Dockerfile.multithread diff --git a/.github/workflows/docker-ghcr-multithread.yml b/.github/workflows/docker-ghcr-multithread.yml new file mode 100644 index 000000000..a7b6dd3fd --- /dev/null +++ b/.github/workflows/docker-ghcr-multithread.yml @@ -0,0 +1,43 @@ +# Build custom Lidarr overlay image and push to GHCR when this branch updates. +name: Docker (multithread) → GHCR + +on: + push: + branches: + - new-multithreaded-import + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/lidarr + +jobs: + build-push: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.multithread + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:new-multithreaded-import + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:git-${{ github.sha }} diff --git a/Dockerfile.multithread b/Dockerfile.multithread new file mode 100644 index 000000000..375a5aeaf --- /dev/null +++ b/Dockerfile.multithread @@ -0,0 +1,19 @@ +# CI / context = repo root (lidarr-src). Local builds from parent folder use ../Dockerfile instead. +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS builder +WORKDIR /src + +COPY . ./ + +RUN dotnet publish src/NzbDrone.Console/Lidarr.Console.csproj \ + -c Release \ + -f net8.0 \ + -r linux-musl-x64 \ + --self-contained true \ + -p:RunAnalyzers=false \ + -p:EnforceCodeStyleInBuild=false \ + -p:TreatWarningsAsErrors=false \ + -o /out + +FROM ghcr.io/linuxserver/lidarr:nightly + +COPY --from=builder /out/ /app/lidarr/bin/ diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index a77432497..3904b6a80 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -5,6 +5,7 @@ using System.IO.Abstractions; using System.Linq; using System.Text.RegularExpressions; +using System.Threading.Tasks; using NLog; using NzbDrone.Common; using NzbDrone.Common.Disk; @@ -84,23 +85,19 @@ public void Scan(List folders = null, FilterFilesType filter = FilterFil } var mediaFileList = new List(); + var mediaFileListLock = new object(); - var musicFilesStopwatch = Stopwatch.StartNew(); - + // Validate folders first (early exit on error like original behaviour) + var foldersToScan = new List(); foreach (var folder in folders) { - // We could be scanning a root folder or a subset of a root folder. If it's a subset, - // check if the root folder exists before cleaning. var rootFolder = _rootFolderService.GetBestRootFolder(folder); - if (rootFolder == null) { _logger.Error("Not scanning {0}, it's not a subdirectory of a defined root folder", folder); return; } - var folderExists = _diskProvider.FolderExists(folder); - if (!folderExists) { if (!_diskProvider.FolderExists(rootFolder.Path)) @@ -110,7 +107,6 @@ public void Scan(List folders = null, FilterFilesType filter = FilterFil skippedArtists.ForEach(x => _eventAggregator.PublishEvent(new ArtistScanSkippedEvent(x, ArtistScanSkippedReason.RootFolderDoesNotExist))); return; } - if (_diskProvider.FolderEmpty(rootFolder.Path)) { _logger.Warn("Artists' root folder ({0}) is empty.", rootFolder.Path); @@ -119,28 +115,35 @@ public void Scan(List folders = null, FilterFilesType filter = FilterFil return; } } - if (!folderExists) { _logger.Debug("Specified scan folder ({0}) doesn't exist.", folder); - CleanMediaFiles(folder, new List()); continue; } + foldersToScan.Add(folder); + } + var musicFilesStopwatch = Stopwatch.StartNew(); + + Parallel.ForEach(foldersToScan, folder => + { _logger.ProgressInfo("Scanning {0}", folder); - var files = FilterFiles(folder, GetAudioFiles(folder)); + var files = FilterFiles(folder, GetAudioFiles(folder)).ToList(); if (!files.Any()) { _logger.Warn("Scan folder {0} is empty.", folder); - continue; + return; } CleanMediaFiles(folder, files.Select(x => x.FullName).ToList()); - mediaFileList.AddRange(files); - } + lock (mediaFileListLock) + { + mediaFileList.AddRange(files); + } + }); var artists = _artistService.GetArtists(artistIds); diff --git a/src/NzbDrone.Core/Music/Services/RefreshArtistService.cs b/src/NzbDrone.Core/Music/Services/RefreshArtistService.cs index 4d4efdf6e..fe1734da2 100644 --- a/src/NzbDrone.Core/Music/Services/RefreshArtistService.cs +++ b/src/NzbDrone.Core/Music/Services/RefreshArtistService.cs @@ -305,6 +305,13 @@ private void RescanArtists(List artists, bool isNew, CommandTrigger trig // badly organized / partly matched libraries folders = artists.Select(x => x.Path).ToList(); } + else if (trigger == CommandTrigger.Manual && artists.Any()) + { + // Manual refresh of specific artist(s): only scan those artists' folders, + // never the entire library (avoids 60k+ file scan when refreshing e.g. Various Artists). + folders = artists.Select(x => x.Path).ToList(); + _logger.Trace("Manual refresh: rescanning only {0} artist folder(s)", folders.Count); + } else if (rescanAfterRefresh == RescanAfterRefreshType.Never) { _logger.Trace("Skipping rescan. Reason: never rescan after refresh");