From 50d729f8a044d8d188bff09bf81cbfc872cde336 Mon Sep 17 00:00:00 2001 From: Seungmin Kim <8457324+ehfd@users.noreply.github.com> Date: Mon, 24 Mar 2025 00:51:33 +0900 Subject: [PATCH] Add ISRC track search for YouTube --- .../sourced_track/sources/youtube.dart | 101 +++++++++++++----- 1 file changed, 75 insertions(+), 26 deletions(-) diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index c4881051..c524b75b 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -240,25 +240,65 @@ class YoutubeSourcedTrack extends SourcedTrack { required Track track, required Ref ref, }) async { + List siblings = []; + + final isrc = track.externalIds?.isrc; + if (isrc != null && isrc.isNotEmpty) { + final isrcResults = + await ref.read(youtubeEngineProvider).searchVideos(isrc.toString()); + if (isrcResults.isNotEmpty) { + final rankedResults = rankResults( + isrcResults.map(YoutubeVideoInfo.fromVideo).toList(), track); + final matchingResults = []; + for (final video in rankedResults) { + final titleWords = video.title + .toLowerCase() + .replaceAll(RegExp(r'[^a-zA-Z0-9\s]+'), '') + .split(RegExp(r'\s+')) + .where((item) => item.isNotEmpty) + .toList(); + final nameLower = track.name! + .toLowerCase() + .replaceAll(RegExp(r'[^a-zA-Z0-9\s]+'), '') + .split(RegExp(r'\s+')) + .where((item) => item.isNotEmpty) + .toList(); + final matchCount = + titleWords.where((word) => nameLower.contains(word)).length; + if (matchCount > nameLower.length / 2) { + matchingResults.add(video); + } + } + if (matchingResults.isNotEmpty) { + final matchingSiblings = + await Future.wait(matchingResults.map((matchingResult) async { + try { + return await toSiblingType(0, matchingResult, ref); + } on VideoUnplayableException catch (e, stack) { + // Ignore this error and continue with the search + AppLogger.reportError(e, stack); + return null; + } + })); + siblings.addAll(matchingSiblings.whereType()); + } + } + } + final links = await SongLinkService.links(track.id!); final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube"); - if (ytLink?.url != null - // allows to fetch siblings more results for already sourced track - && - track is! SourcedTrack) { + if (ytLink?.url != null) { try { - return [ - await toSiblingType( - 0, - YoutubeVideoInfo.fromVideo( - await ref.read(youtubeEngineProvider).getVideo( - Uri.parse(ytLink!.url!).queryParameters["v"]!, - ), - ), - ref, - ) - ]; + siblings.add(await toSiblingType( + 0, + YoutubeVideoInfo.fromVideo( + await ref.read(youtubeEngineProvider).getVideo( + Uri.parse(ytLink!.url!).queryParameters["v"]!, + ), + ), + ref, + )); } on VideoUnplayableException catch (e, stack) { // Ignore this error and continue with the search AppLogger.reportError(e, stack); @@ -271,20 +311,29 @@ class YoutubeSourcedTrack extends SourcedTrack { await ref.read(youtubeEngineProvider).searchVideos(query); if (ServiceUtils.onlyContainsEnglish(query)) { - return await Future.wait(searchResults + siblings.addAll(await Future.wait(searchResults .map(YoutubeVideoInfo.fromVideo) - .mapIndexed((index, info) => toSiblingType(index, info, ref))); + .mapIndexed((index, info) => toSiblingType(index, info, ref)))); + } else { + final rankedSiblings = rankResults( + searchResults.map(YoutubeVideoInfo.fromVideo).toList(), + track, + ); + siblings.addAll(await Future.wait( + rankedSiblings + .mapIndexed((index, info) => toSiblingType(index, info, ref)), + )); } - final rankedSiblings = rankResults( - searchResults.map(YoutubeVideoInfo.fromVideo).toList(), - track, - ); - - return await Future.wait( - rankedSiblings - .mapIndexed((index, info) => toSiblingType(index, info, ref)), - ); + final seenIds = {}; + // Deduplicate siblings by info.id, keeping the first occurrence + return await Future.wait(siblings.map((sibling) async { + if (!seenIds.contains(sibling.info.id)) { + seenIds.add(sibling.info.id); + return sibling; + } + return null; + })).then((s) => s.whereType().toList()); } @override