Skip to content

Commit

Permalink
Improve youtube connector
Browse files Browse the repository at this point in the history
Parse description to get track info.
Remove YouTube Topic support.

Resolves #1896.
  • Loading branch information
alexesprit committed Nov 3, 2019
1 parent 6116c2f commit c78c372
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 30 deletions.
54 changes: 26 additions & 28 deletions src/connectors/youtube.js
Expand Up @@ -22,17 +22,6 @@ const CATEGORIES = [
CATEGORY_SCIENCE_AND_TECHNOLOGY
];

// Basic support for languages the extension is traslated to.
// TODO: Remove if description parsing is implemented.
const TOPIC_SUFFIXES = [
/(.+) - Topic/, // EN
/(.+) – Thema/, // DE
/(.+) – тема/, // RU
/(.+) - Tema/, // ES
/(.+) – temat/, // PL
/(.+) – Tópico/, // PT
];

/**
* Array of categories allowed to be scrobbled.
* @type {Array}
Expand All @@ -54,11 +43,19 @@ const videoSelector = '.html5-main-video';
const videoTitleSelector = '.html5-video-player .ytp-title-link';
const channelNameSelector = '#top-row .ytd-channel-name a';

let currentVideoDescription = null;
let artistTrackFromDescription = null;

readConnectorOptions();

Connector.playerSelector = '#content';

Connector.getArtistTrack = () => {
Connector.getTrackInfo = () => {
const trackInfo = getTrackInfoFromDescription();
if (!Util.isArtistTrackEmpty(trackInfo)) {
return trackInfo;
}

let { artist, track } = Util.processYoutubeVideoTitle(
Util.getTextFromSelectors(videoTitleSelector)
);
Expand Down Expand Up @@ -127,11 +124,7 @@ Connector.isScrobblingAllowed = () => {
return true;
};

Connector.applyFilter(
MetadataFilter.getYoutubeFilter().append({
artist: removeTopicSuffix,
})
);
Connector.applyFilter(MetadataFilter.getYoutubeFilter());

/**
* Get video category.
Expand Down Expand Up @@ -164,17 +157,6 @@ function getVideoCategory(videoId) {
return categoryCache.get(videoId);
}

function removeTopicSuffix(text) {
for (const regex of TOPIC_SUFFIXES) {
const topicMatch = text.match(regex);
if (topicMatch) {
return topicMatch[1];
}
}

return text;
}

async function fetchCategoryId() {
await fillMoreSection();

Expand Down Expand Up @@ -246,3 +228,19 @@ async function readConnectorOptions() {
allowedCategories.push(CATEGORY_ENTERTAINMENT);
}
}

function getVideoDescription() {
return $('#description').text();
}

function getTrackInfoFromDescription() {
const description = getVideoDescription();
if (currentVideoDescription === description) {
return artistTrackFromDescription;
}

currentVideoDescription = description;
artistTrackFromDescription = Util.parseYtVideoDescription(description);

return artistTrackFromDescription;
}
48 changes: 48 additions & 0 deletions src/core/content/util.js
Expand Up @@ -505,6 +505,54 @@ const Util = {
* @type {String}
*/
ARTIST_SEPARATOR: ', ',

descFirstLine: 'Provided to YouTube',

descLastLine: 'Auto-generated by YouTube.',

descArtistTrackSeparator: ' · ',

isYtVideoDescriptionValid(desc) {
return desc && (
desc.startsWith(this.descFirstLine) ||
desc.endsWith(this.descLastLine)
);
},

parseYtVideoDescription(desc) {
if (!this.isYtVideoDescriptionValid(desc)) {
return null;
}

const lines = desc.split('\n').filter((line) => {
return line.length > 0;
}).filter((line) => {
return !line.startsWith(this.descFirstLine);
});

const firstLine = lines[0];
const secondLine = lines[1];

const trackInfo = firstLine.split(this.descArtistTrackSeparator);
const numberOfFields = trackInfo.length;

const album = secondLine;
let artist = null;
let track = null;
let featArtists = null;

if (numberOfFields === 2) {
[track, artist] = trackInfo;
} else if (numberOfFields > 2) {
[track, artist, featArtists] = trackInfo;

const featArtistsStr = featArtists.split(this.ytDescSeparator)
.join(this.ARTIST_SEPARATOR);
track = `${track} (feat. ${featArtistsStr})`;
}

return { artist, track, album };
},
};

/**
Expand Down
95 changes: 93 additions & 2 deletions tests/content/util.js
Expand Up @@ -295,6 +295,80 @@ const IS_ARTIST_TRACK_EMPTY_DATA = [{
expected: false
}];

const YT_DESCRIPTION_EXAMPLE_1 = `Provided to YouTube by IDOL
Tranquility (Forces of Nature Remix) · Aquasky
Shadow Era, Pt. 2
℗ Passenger Ltd
Released on: 2015-10-09
Lyricist: B. Newitt
Lyricist: D. Wallace
Lyricist: K. Bailey
Composer: B. Newitt
Composer: D. Wallace
Composer: K. Bailey
Auto-generated by YouTube.`;

const YT_DESCRIPTION_EXAMPLE_2 = `Capoeira (Airbase pres. Scarab Remix) · Under Sun
Capoeira
℗ Armada Music B.V.
Released on: 2006-08-21
A R T I S T: Under Sun
Auto-generated by YouTube.`;

const YT_DESCRIPTION_EXAMPLE_3 = `Provided to YouTube by The Orchard Enterprises
Fugitive · Time, The Valuator · Mattéo Gelsomino
How Fleeting, How Fragile
℗ 2018 Long Branch Records
Released on: 2018-08-03
Auto-generated by YouTube.`;

const PARSE_YT_VIDEO_DESCRIPTION_DATA = [{
description: 'should not parse null description',
source: null,
expected: null
}, {
description: 'should parse normal description',
source: YT_DESCRIPTION_EXAMPLE_1,
expected: {
album: 'Shadow Era, Pt. 2',
track: 'Tranquility (Forces of Nature Remix)',
artist: 'Aquasky',
}
}, {
description: 'should parse description w/o "Autogenerated" header',
source: YT_DESCRIPTION_EXAMPLE_2,
expected: {
album: 'Capoeira',
track: 'Capoeira (Airbase pres. Scarab Remix)',
artist: 'Under Sun',
}
}, {
description: 'should parse description w/ featuring artists',
source: YT_DESCRIPTION_EXAMPLE_3,
expected: {
album: 'How Fleeting, How Fragile',
track: 'Fugitive (feat. Mattéo Gelsomino)',
artist: 'Time, The Valuator',
}
}];


/**
* Test 'Util.splitArtistTrack' function.
*/
Expand Down Expand Up @@ -401,20 +475,36 @@ function testIsArtistTrackEmpty() {
testFunction(Util.isArtistTrackEmpty, IS_ARTIST_TRACK_EMPTY_DATA);
}

/**
* Test 'Util.parseYtVideoDescription' function.
*/
function testParseYtVideoDescription() {
testFunction(
Util.parseYtVideoDescription,
PARSE_YT_VIDEO_DESCRIPTION_DATA,
{ isDeepEqual: true }
);
}

/**
* Test function.
* @param {Function} func Function to be tested
* @param {Array} testData Array of test data
* @param {Boolean} isDeepEqual Should use deep equal
*/
function testFunction(func, testData) {
function testFunction(func, testData, { isDeepEqual = false } = {}) {
const boundFunc = func.bind(Util);

for (let data of testData) {
let { description, source, expected } = data;
let actual = boundFunc(source);

it(description, function() {
expect(actual).to.be.equal(expected);
if (isDeepEqual) {
expect(actual).to.be.deep.equal(expected);
} else {
expect(actual).to.be.equal(expected);
}
});
}
}
Expand All @@ -429,6 +519,7 @@ function runTests() {
describe('splitArtistTrack', testSplitArtistTrack);
describe('isArtistTrackEmpty', testIsArtistTrackEmpty);
describe('escapeBadTimeValues', testEscapeBadTimeValues);
describe('parseYtVideoDescription', testParseYtVideoDescription);
describe('extractUrlFromCssProperty', testExtractTrackArtFromCss);
describe('processYoutubeVideoTitle', testProcessYoutubeVideoTitle);
describe('getYoutubeVideoIdFromUrl', testGetYoutubeVideoIdFromUrl);
Expand Down

2 comments on commit c78c372

@Paszt
Copy link
Contributor

@Paszt Paszt commented on c78c372 Nov 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aww, why remove topic support? That pretty much renders the extension useless for me now.

@alexesprit
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Paszt I removed "Topic" support, because I believe all topic channels contain auto-generated videos. Am I wrong?

Please sign in to comment.