@@ -3,13 +3,17 @@ package unpackerr
33import (
44 "errors"
55 "path/filepath"
6+ "regexp"
67 "strings"
78 "time"
89
910 "golift.io/starr"
1011 "golift.io/starr/lidarr"
1112)
1213
14+ // numberedTrackPattern matches filenames generated by CUE splitting: "NN - Title.flac".
15+ var numberedTrackPattern = regexp .MustCompile (`^\d{2,3} - .+\.flac$` )
16+
1317// LidarrConfig represents the input data for a Lidarr server.
1418type LidarrConfig struct {
1519 StarrConfig
@@ -102,6 +106,7 @@ func (u *Unpackerr) checkLidarrQueue(now time.Time) {
102106 Syncthing : server .Syncthing ,
103107 SplitFlac : server .SplitFlac ,
104108 Path : u .getDownloadPath (record .OutputPath , starr .Lidarr , record .Title , server .Paths ),
109+ OutputPath : record .OutputPath ,
105110 IDs : map [string ]any {
106111 "title" : record .Title ,
107112 "artistId" : record .ArtistID ,
@@ -150,36 +155,32 @@ func (u *Unpackerr) lidarrServerByURL(url string) *LidarrConfig {
150155 return nil
151156}
152157
153- // extractionHasFlacFiles returns true if any path in files has a .flac extension.
154- // Used to only trigger manual import after a FLAC+CUE split, not for e.g. zip-of-mp3s.
155- func extractionHasFlacFiles (files []string ) bool {
156- for _ , p := range files {
157- if strings .HasSuffix (strings .ToLower (p ), ".flac" ) {
158- return true
159- }
160- }
161-
162- return false
158+ // crossPlatformBase returns the filename from a path that may use either forward slashes
159+ // or backslashes as separators. On Linux, filepath.Base does not split on backslashes,
160+ // so Windows/UNC paths from Starr apps would return the entire path instead of the filename.
161+ func crossPlatformBase (path string ) string {
162+ return filepath .Base (filepath .FromSlash (strings .ReplaceAll (path , `\` , `/` )))
163163}
164164
165- // filterManualImportToSplitTracks returns only outputs whose path is in the extract's NewFiles
166- // (the split track files). This excludes the original FLAC file we split from, which Lidarr
167- // would otherwise try and fail to import.
165+ // filterManualImportToSplitTracks returns only outputs whose filename matches a split track file.
166+ // This excludes the original audio file we split from, which Lidarr would otherwise try and fail
167+ // to import. Comparison uses basenames (filenames only) so it works across platforms — the extraction
168+ // host (e.g. Linux) may use different paths than the Starr app (e.g. Windows).
168169func filterManualImportToSplitTracks (outputs []* lidarr.ManualImportOutput , item * Extract ) []* lidarr.ManualImportOutput {
169170 if item == nil || item .Resp == nil || len (item .Resp .NewFiles ) == 0 {
170- return outputs
171+ return nil // Signal that we couldn't filter; caller should try fallback.
171172 }
172173
173- splitPaths := make (map [string ]struct {}, len (item .Resp .NewFiles ))
174+ splitFiles := make (map [string ]struct {}, len (item .Resp .NewFiles ))
174175 for _ , p := range item .Resp .NewFiles {
175- splitPaths [ filepath . Clean ( p )] = struct {}{}
176+ splitFiles [ strings . ToLower ( crossPlatformBase ( p ) )] = struct {}{}
176177 }
177178
178179 filtered := outputs [:0 ] // reusable memory
179180
180181 for _ , out := range outputs {
181182 if out != nil && out .Path != "" {
182- if _ , ok := splitPaths [ filepath . Clean ( out .Path )]; ok {
183+ if _ , ok := splitFiles [ strings . ToLower ( crossPlatformBase ( out .Path ) )]; ok {
183184 filtered = append (filtered , out )
184185 }
185186 }
@@ -188,6 +189,43 @@ func filterManualImportToSplitTracks(outputs []*lidarr.ManualImportOutput, item
188189 return filtered
189190}
190191
192+ // filterManualImportToNumberedTracks returns outputs whose filename matches the CUE-split
193+ // naming pattern (NN - Title.flac). This is a fallback when NewFiles tracking is unavailable
194+ // (e.g. after MoveFiles moves tracks back to the original folder).
195+ func filterManualImportToNumberedTracks (outputs []* lidarr.ManualImportOutput ) []* lidarr.ManualImportOutput {
196+ filtered := make ([]* lidarr.ManualImportOutput , 0 , len (outputs ))
197+
198+ for _ , out := range outputs {
199+ if out != nil && out .Path != "" {
200+ base := crossPlatformBase (out .Path )
201+ if numberedTrackPattern .MatchString (base ) {
202+ filtered = append (filtered , out )
203+ }
204+ }
205+ }
206+
207+ return filtered
208+ }
209+
210+ // filterSplitOutputs filters ManualImport outputs to only include split track files.
211+ // First tries matching against NewFiles (basename comparison), then falls back to the
212+ // CUE-split naming pattern (NN - Title.flac) when NewFiles is unavailable.
213+ func (u * Unpackerr ) filterSplitOutputs (
214+ outputs []* lidarr.ManualImportOutput , item * Extract ,
215+ ) []* lidarr.ManualImportOutput {
216+ allOutputs := make ([]* lidarr.ManualImportOutput , len (outputs ))
217+ copy (allOutputs , outputs )
218+
219+ filtered := filterManualImportToSplitTracks (allOutputs , item )
220+ if len (filtered ) > 0 {
221+ return filtered
222+ }
223+
224+ u .Printf ("[Lidarr] No split track files matched via NewFiles for %s; trying numbered track pattern" , item .Path )
225+
226+ return filterManualImportToNumberedTracks (allOutputs )
227+ }
228+
191229// importSplitFlacTracks runs in a goroutine after a Lidarr FLAC+CUE split extraction completes.
192230// It asks Lidarr for the manual import list for the extract folder and sends the ManualImport command
193231// so Lidarr imports the split track files.
@@ -200,15 +238,20 @@ func (u *Unpackerr) importSplitFlacTracks(item *Extract, server *LidarrConfig) {
200238 downloadID , _ := item .IDs ["downloadId" ].(string )
201239 artistID , _ := item .IDs ["artistId" ].(int64 )
202240
203- params := & lidarr.ManualImportParams {
204- Folder : item .Path ,
241+ // Use OutputPath (the Starr app's view of the path) for ManualImport when available.
242+ // The extraction host may use a different mount/path than the Starr app (e.g., Linux vs Windows UNC).
243+ importFolder := item .Path
244+ if item .OutputPath != "" {
245+ importFolder = item .OutputPath
246+ }
247+
248+ outputs , err := server .ManualImport (& lidarr.ManualImportParams {
249+ Folder : importFolder ,
205250 DownloadID : downloadID ,
206251 ArtistID : artistID ,
207252 FilterExistingFiles : false ,
208253 ReplaceExistingFiles : true ,
209- }
210-
211- outputs , err := server .ManualImport (params )
254+ })
212255 if err != nil {
213256 u .Errorf ("[Lidarr] Manual import list failed for %s: %v" , item .Path , err )
214257 return
@@ -219,12 +262,9 @@ func (u *Unpackerr) importSplitFlacTracks(item *Extract, server *LidarrConfig) {
219262 return
220263 }
221264
222- // Exclude the original FLAC we split from; only import the split track files (item.Resp.NewFiles).
223- // Lidarr returns every file in the folder including the source file, which will never import.
224- outputs = filterManualImportToSplitTracks (outputs , item )
225-
265+ outputs = u .filterSplitOutputs (outputs , item )
226266 if len (outputs ) == 0 {
227- u .Printf ("[Lidarr] No split track files to import (folder: %s); original FLAC excluded " , item .Path )
267+ u .Printf ("[Lidarr] No split track files to import (folder: %s)" , item .Path )
228268 return
229269 }
230270
@@ -234,11 +274,10 @@ func (u *Unpackerr) importSplitFlacTracks(item *Extract, server *LidarrConfig) {
234274 return
235275 }
236276
237- u .Debugf ("[Lidarr] Sending manual import command: replaceExisting=%v, importMode=%q, files=%d: %v " ,
238- cmd .ReplaceExistingFiles , cmd .ImportMode , len (cmd .Files ), cmd . Files )
277+ u .Debugf ("[Lidarr] Sending manual import command: replaceExisting=%v, importMode=%q, files=%d" ,
278+ cmd .ReplaceExistingFiles , cmd .ImportMode , len (cmd .Files ))
239279
240- _ , err = server .SendManualImportCommand (cmd )
241- if err != nil {
280+ if _ , err = server .SendManualImportCommand (cmd ); err != nil {
242281 u .Errorf ("[Lidarr] Manual import command failed for %s: %v" , item .Path , err )
243282 return
244283 }
0 commit comments