Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Track Matching #1382

Merged
merged 6 commits into from
Jan 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions packages/core/src/rest/Youtube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function analyseUrlType(url) {

function formatPlaylistTrack(track: ytpl.Item) {
return {
streams: [{source: 'Youtube', id: track.id}],
streams: [{ source: 'Youtube', id: track.id }],
name: track.title,
thumbnail: track.thumbnails[0].url,
artist: track.author.name
Expand Down Expand Up @@ -89,12 +89,12 @@ async function handleYoutubeVideo(url: string): Promise<YoutubeResult[]> {
const videoDetails = info.videoDetails;

return [{
streams: [{source: 'Youtube', id: videoDetails.videoId}],
streams: [{ source: 'Youtube', id: videoDetails.videoId }],
name: videoDetails.title,
thumbnail: videoDetails.thumbnails[0].url,
artist: {name: videoDetails.ownerChannelName}
artist: { name: videoDetails.ownerChannelName }
}];
}
}
return [];
})
.catch(function () {
Expand Down Expand Up @@ -129,10 +129,10 @@ export async function liveStreamSearch(query: string): Promise<YoutubeResult[]>

return searchResults.items.map((video: ytsr.Video) => {
return {
streams: [{source: 'Youtube', id: video.id}],
streams: [{ source: 'Youtube', id: video.id }],
name: video.title,
thumbnail: video.bestThumbnail.url,
artist: {name: video.author.name}
artist: { name: video.author.name }
};
});
}
Expand All @@ -145,15 +145,20 @@ export async function trackSearchByString(query: StreamQuery, sourceName?: strin
const terms = query.artist + ' ' + query.track;
const filterOptions = await ytsr.getFilters(terms);
const filterVideoOnly = filterOptions.get('Type').get('Video');
const results = await ytsr(filterVideoOnly.url, { limit: 15 });
const results = await ytsr(filterVideoOnly.url, { limit: 10 });
const heuristics = new YoutubeHeuristics();

const orderedTracks = heuristics.orderTracks({
let orderedTracks = heuristics.orderTracks({
tracks: results.items as ytsr.Video[],
artist: query.artist,
title: query.track
});

// [HACK] Currently a bug exists where some tracks are returned as not defined. If this happens to the top track loading the track will fail...
// This is a HACK to ensure that doesn't happen
while (orderedTracks[0].id === undefined) {
orderedTracks = orderedTracks.slice(1);
}
return [
await getStreamForId(orderedTracks[0].id, sourceName),
...orderedTracks
Expand All @@ -168,7 +173,7 @@ export const getStreamForId = async (id: string, sourceName: string, useSponsorB
const trackInfo = await ytdl.getInfo(videoUrl);
const formatInfo = ytdl.chooseFormat(trackInfo.formats, { quality: 'highestaudio' });
const segments = useSponsorBlock ? await SponsorBlock.getSegments(id) : [];

return {
id,
source: sourceName,
Expand Down Expand Up @@ -199,12 +204,12 @@ function videoToStreamData(video: ytsr.Video, source: string): StreamData {
stream: undefined,
duration: parseInt(video.duration),
title: video.title,
thumbnail: video.bestThumbnail.url,
thumbnail: video.bestThumbnail?.url,
originalUrl: video.url,
isLive: video.isLive,
author: {
name: video.author.name,
thumbnail: video.author.bestAvatar.url
name: video.author?.name,
thumbnail: video.author?.bestAvatar.url
}
};
}
38 changes: 19 additions & 19 deletions packages/core/src/rest/heuristics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ import { Video } from 'ytsr';
import levenshtein from 'fast-levenshtein';

type ScoreTrackArgs<Track> = {
track: Track;
artist: string;
title: string;
duration?: number;
track: Track;
artist: string;
title: string;
duration?: number;
}

type OrderTracksArgs<Track> = {
tracks: Track[];
artist: string;
title: string;
duration?: number;
tracks: Track[];
artist: string;
title: string;
duration?: number;
};

interface SearchHeuristics<Track> {
scoreTrack: (args: ScoreTrackArgs<Track>) => number;
orderTracks: (args: OrderTracksArgs<Track>) => Track[];
scoreTrack: (args: ScoreTrackArgs<Track>) => number;
orderTracks: (args: OrderTracksArgs<Track>) => Track[];
}

const penalizedWords = [
Expand All @@ -35,7 +35,7 @@ const promotedWords = [

export class YoutubeHeuristics implements SearchHeuristics<Partial<Video>> {
static createTitle = ({
artist,
artist,
title
}: { artist: string; title: string; }) => `${artist} - ${title}`.toLowerCase();

Expand All @@ -47,27 +47,27 @@ export class YoutubeHeuristics implements SearchHeuristics<Partial<Video>> {
}: ScoreTrackArgs<Partial<Video>>) => {
const trackTitle = YoutubeHeuristics.createTitle({ artist, title });
const lowercaseResultTitle = track.title.toLowerCase();
const titleScore = (1 - (levenshtein.get(trackTitle, lowercaseResultTitle)/trackTitle.length)) * 100;
const verbatimSubstringScore = lowercaseResultTitle.includes(trackTitle) ? 100 : 0;

const titleScore = (1 - (levenshtein.get(trackTitle, lowercaseResultTitle) / trackTitle.length)) * 25;

const verbatimSubstringScore = lowercaseResultTitle.includes(title.toLowerCase()) ? 300 : 0;

const durationDelta = Math.abs(Number.parseFloat(track.duration) - duration);
const durationScore = track.duration && duration
? (1 - durationDelta/duration) * 100
? (1 - durationDelta / duration) * 800
: 0;

const promotedWordsScore = promotedWords.some(promotedWord => lowercaseResultTitle.includes(promotedWord)) ? 100 : 0;

const penalizedWordsScore = penalizedWords.some(word => lowercaseResultTitle.includes(word)) ? 0 : 100;
const penalizedWordsScore = penalizedWords.some(word => lowercaseResultTitle.includes(word)) ? 0 : 200;

let liveVideoScore = 100;
if (lowercaseResultTitle.includes('live') && !trackTitle.includes('live')) {
liveVideoScore = 0;
}

const channelNameScore = track.author.name.toLowerCase().includes(artist.toLowerCase()) ? 200 : 0;
const verifiedChannelScore = track.author.verified ? 200 : 0;
const channelNameScore = track.author?.name.toLowerCase().includes(artist.toLowerCase()) ? 300 : 0;
const verifiedChannelScore = track.author?.verified ? 200 : 0;

return mean([
titleScore,
Expand Down