|
|
@@ -109,6 +109,10 @@ export struct NavidromePage {
|
|
|
private albumCoverLookup: Map<string, string> = new Map();
|
|
|
private searchTicket: number = 0;
|
|
|
private readonly REMOTE_PAGE_SIZE: number = 200;
|
|
|
+ private jellyfinSongIdSeen: Set<string> = new Set();
|
|
|
+ private embySongIdSeen: Set<string> = new Set();
|
|
|
+ private jellyfinSongKeySeen: Set<string> = new Set();
|
|
|
+ private embySongKeySeen: Set<string> = new Set();
|
|
|
|
|
|
private isNavidromeAccount(account: WebDavAccount): boolean {
|
|
|
return account.webType === RemoteDriveType.Navidrome;
|
|
|
@@ -268,6 +272,10 @@ export struct NavidromePage {
|
|
|
this.artists = [];
|
|
|
this.albums = [];
|
|
|
this.playlists = [];
|
|
|
+ this.jellyfinSongIdSeen.clear();
|
|
|
+ this.embySongIdSeen.clear();
|
|
|
+ this.jellyfinSongKeySeen.clear();
|
|
|
+ this.embySongKeySeen.clear();
|
|
|
this.clearFilter();
|
|
|
this.coverUrlCache.clear();
|
|
|
this.albumCoverLookup.clear();
|
|
|
@@ -512,12 +520,26 @@ export struct NavidromePage {
|
|
|
if (currentTicket !== this.loadTicket) {
|
|
|
return;
|
|
|
}
|
|
|
- const restSongs = this.convertJellyfinSongsToRestSongs(response.items);
|
|
|
+ const uniqueItems: JellyfinSong[] = [];
|
|
|
+ for (let i = 0; i < response.items.length; i++) {
|
|
|
+ const item = response.items[i];
|
|
|
+ if (!item.id) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const key = this.buildSongDedupKey(item.title, item.artist, item.album, item.durationSeconds);
|
|
|
+ if (this.jellyfinSongIdSeen.has(item.id) || this.jellyfinSongKeySeen.has(key)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ this.jellyfinSongIdSeen.add(item.id);
|
|
|
+ this.jellyfinSongKeySeen.add(key);
|
|
|
+ uniqueItems.push(item);
|
|
|
+ }
|
|
|
+ const restSongs = this.convertJellyfinSongsToRestSongs(uniqueItems);
|
|
|
const videoItems = await this.convertSongsToVideoItems(restSongs, account);
|
|
|
if (currentTicket !== this.loadTicket) {
|
|
|
return;
|
|
|
}
|
|
|
- this.allVideos = [...this.allVideos, ...videoItems];
|
|
|
+ this.allVideos = this.dedupRemoteSongsByKey([...this.allVideos, ...videoItems]);
|
|
|
this.songNextStart = response.nextStart;
|
|
|
void ServerLogUtil.info('NavidromeLoad', `Jellyfin 歌曲追加: 本次 ${videoItems.length} 首, 总数 ${this.allVideos.length}`);
|
|
|
} finally {
|
|
|
@@ -582,12 +604,26 @@ export struct NavidromePage {
|
|
|
if (currentTicket !== this.loadTicket) {
|
|
|
return;
|
|
|
}
|
|
|
- const restSongs = this.convertEmbySongsToRestSongs(response.items);
|
|
|
+ const uniqueItems: EmbySong[] = [];
|
|
|
+ for (let i = 0; i < response.items.length; i++) {
|
|
|
+ const item = response.items[i];
|
|
|
+ if (!item.id) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const key = this.buildSongDedupKey(item.title, item.artist, item.album, item.durationSeconds);
|
|
|
+ if (this.embySongIdSeen.has(item.id) || this.embySongKeySeen.has(key)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ this.embySongIdSeen.add(item.id);
|
|
|
+ this.embySongKeySeen.add(key);
|
|
|
+ uniqueItems.push(item);
|
|
|
+ }
|
|
|
+ const restSongs = this.convertEmbySongsToRestSongs(uniqueItems);
|
|
|
const videoItems = await this.convertSongsToVideoItems(restSongs, account);
|
|
|
if (currentTicket !== this.loadTicket) {
|
|
|
return;
|
|
|
}
|
|
|
- this.allVideos = [...this.allVideos, ...videoItems];
|
|
|
+ this.allVideos = this.dedupRemoteSongsByKey([...this.allVideos, ...videoItems]);
|
|
|
this.songNextStart = response.nextStart;
|
|
|
void ServerLogUtil.info('NavidromeLoad', `Emby 歌曲追加: 本次 ${videoItems.length} 首, 总数 ${this.allVideos.length}`);
|
|
|
} finally {
|
|
|
@@ -973,6 +1009,41 @@ export struct NavidromePage {
|
|
|
return results;
|
|
|
}
|
|
|
|
|
|
+ private buildSongDedupKey(title?: string, artist?: string, album?: string, durationSeconds?: number): string {
|
|
|
+ const safeTitle = title ?? '';
|
|
|
+ const safeArtist = artist ?? '';
|
|
|
+ const safeAlbum = album ?? '';
|
|
|
+ const safeDuration = durationSeconds !== undefined && durationSeconds !== null ? durationSeconds.toString() : '';
|
|
|
+ return `${safeTitle}__${safeArtist}__${safeAlbum}__${safeDuration}`.toLowerCase();
|
|
|
+ }
|
|
|
+
|
|
|
+ private buildSongKeyFromItem(item: VideoItem): string {
|
|
|
+ const safeTitle = item.name ?? '';
|
|
|
+ const safeArtist = item.artist ?? '';
|
|
|
+ const safeAlbum = item.album ?? '';
|
|
|
+ const safeDuration = item.duration ?? '';
|
|
|
+ return `${safeTitle}__${safeArtist}__${safeAlbum}__${safeDuration}`.toLowerCase();
|
|
|
+ }
|
|
|
+
|
|
|
+ private dedupRemoteSongsByKey(items: VideoItem[]): VideoItem[] {
|
|
|
+ const map = new Map<string, VideoItem>();
|
|
|
+ for (let i = 0; i < items.length; i++) {
|
|
|
+ const item = items[i];
|
|
|
+ const key = this.buildSongKeyFromItem(item);
|
|
|
+ const existing = map.get(key);
|
|
|
+ if (!existing) {
|
|
|
+ map.set(key, item);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const existingHasAlbumId = StrUtil.isNotEmpty(existing.navAlbumId);
|
|
|
+ const currentHasAlbumId = StrUtil.isNotEmpty(item.navAlbumId);
|
|
|
+ if (!existingHasAlbumId && currentHasAlbumId) {
|
|
|
+ map.set(key, item);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return Array.from(map.values());
|
|
|
+ }
|
|
|
+
|
|
|
private convertApiSongsToRestSongs(songs: NavidromeSong[]): NavidromeRestSong[] {
|
|
|
return songs.map((song: NavidromeSong): NavidromeRestSong => ({
|
|
|
id: song.id,
|