feat(quick-8): add FindDuplicateTracksInPlaylist backend method

- Add DuplicateTrackInfo and DuplicateCheckResult types
- Implement FindDuplicateTracksInPlaylist on playlist Service
- Regenerate Wails TypeScript bindings
This commit is contained in:
2026-03-01 11:12:04 -05:00
parent 5ab214baed
commit 83de934c39
4 changed files with 134 additions and 0 deletions
+75
View File
@@ -98,6 +98,23 @@ type PhantomSearchResult struct {
Unmatched []string `json:"Unmatched"` Unmatched []string `json:"Unmatched"`
} }
// DuplicateTrackInfo holds metadata for a track that already
// exists in a playlist.
type DuplicateTrackInfo struct {
FilePath string `json:"FilePath"`
Title string `json:"Title"`
Artist string `json:"Artist"`
Album string `json:"Album"`
Duration string `json:"Duration"`
}
// DuplicateCheckResult contains the outcome of checking for
// duplicate tracks in a playlist.
type DuplicateCheckResult struct {
Duplicates []DuplicateTrackInfo `json:"Duplicates"`
Unique []string `json:"Unique"`
}
// Service manages playlist operations. // Service manages playlist operations.
type Service struct { type Service struct {
// mu protects ctx and favoritesConf from concurrent access // mu protects ctx and favoritesConf from concurrent access
@@ -509,6 +526,64 @@ func (s *Service) AddTracksToPlaylist(
return nil return nil
} }
// FindDuplicateTracksInPlaylist checks which of the given file
// paths already exist in the specified playlist. Returns metadata
// for each duplicate and a list of non-duplicate file paths.
func (s *Service) FindDuplicateTracksInPlaylist(
playlistID int64,
filePaths []string,
) (DuplicateCheckResult, error) {
rows, err := s.db.Queries.GetPlaylistTracksWithMetadata(
s.db.Ctx,
playlistID,
)
if err != nil {
s.logger.Error(
"Failed to get playlist tracks for duplicate check",
"playlistId", playlistID,
"err", err,
)
return DuplicateCheckResult{}, fmt.Errorf(
"failed to get playlist tracks: %w", err,
)
}
existingPaths := make(
map[string]sqlcgen.GetPlaylistTracksWithMetadataRow,
len(rows),
)
for _, row := range rows {
existingPaths[row.FilePath] = row
}
var duplicates []DuplicateTrackInfo
var unique []string
for _, fp := range filePaths {
if row, exists := existingPaths[fp]; exists {
duplicates = append(duplicates, DuplicateTrackInfo{
FilePath: fp,
Title: row.Title,
Artist: row.Artist,
Album: row.Album,
Duration: strconv.FormatInt(
row.LengthMilliseconds, 10,
),
})
} else {
unique = append(unique, fp)
}
}
return DuplicateCheckResult{
Duplicates: duplicates,
Unique: unique,
}, nil
}
// CreatePlaylistWithTracks creates a new playlist and populates // CreatePlaylistWithTracks creates a new playlist and populates
// it with tracks. // it with tracks.
func (s *Service) CreatePlaylistWithTracks( func (s *Service) CreatePlaylistWithTracks(
+53
View File
@@ -236,6 +236,59 @@ export namespace playlist {
this.Score = source["Score"]; this.Score = source["Score"];
} }
} }
export class DuplicateTrackInfo {
FilePath: string;
Title: string;
Artist: string;
Album: string;
Duration: string;
static createFrom(source: any = {}) {
return new DuplicateTrackInfo(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.FilePath = source["FilePath"];
this.Title = source["Title"];
this.Artist = source["Artist"];
this.Album = source["Album"];
this.Duration = source["Duration"];
}
}
export class DuplicateCheckResult {
Duplicates: DuplicateTrackInfo[];
Unique: string[];
static createFrom(source: any = {}) {
return new DuplicateCheckResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.Duplicates = this.convertValues(source["Duplicates"], DuplicateTrackInfo);
this.Unique = source["Unique"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class PhantomMatch { export class PhantomMatch {
PhantomPath: string; PhantomPath: string;
PhantomTitle: string; PhantomTitle: string;
+2
View File
@@ -15,6 +15,8 @@ export function DeletePlaylist(arg1:number):Promise<void>;
export function EnsureDefaultPlaylist():Promise<void>; export function EnsureDefaultPlaylist():Promise<void>;
export function FindDuplicateTracksInPlaylist(arg1:number,arg2:Array<string>):Promise<playlist.DuplicateCheckResult>;
export function FindPhantomMatches(arg1:number,arg2:Array<string>):Promise<playlist.PhantomSearchResult>; export function FindPhantomMatches(arg1:number,arg2:Array<string>):Promise<playlist.PhantomSearchResult>;
export function GetAllPlaylists():Promise<Array<playlist.Summary>>; export function GetAllPlaylists():Promise<Array<playlist.Summary>>;
+4
View File
@@ -26,6 +26,10 @@ export function EnsureDefaultPlaylist() {
return window['go']['playlist']['Service']['EnsureDefaultPlaylist'](); return window['go']['playlist']['Service']['EnsureDefaultPlaylist']();
} }
export function FindDuplicateTracksInPlaylist(arg1, arg2) {
return window['go']['playlist']['Service']['FindDuplicateTracksInPlaylist'](arg1, arg2);
}
export function FindPhantomMatches(arg1, arg2) { export function FindPhantomMatches(arg1, arg2) {
return window['go']['playlist']['Service']['FindPhantomMatches'](arg1, arg2); return window['go']['playlist']['Service']['FindPhantomMatches'](arg1, arg2);
} }