Skip to content

Commit

Permalink
fix(youtube): Implement bump-only list change detection
Browse files Browse the repository at this point in the history
Should fix new way YTM orders recent history #195
  • Loading branch information
FoxxMD committed Oct 15, 2024
1 parent 72d79fc commit 0ec701b
Show file tree
Hide file tree
Showing 3 changed files with 271 additions and 54 deletions.
42 changes: 26 additions & 16 deletions src/backend/sources/YTMusicSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getPlaysDiff,
humanReadableDiff,
playsAreAddedOnly,
playsAreBumpedOnly,
playsAreSortConsistent
} from "../utils/PlayComparisonUtils.js";
import AbstractSource, { RecentlyPlayedOptions } from "./AbstractSource.js";
Expand Down Expand Up @@ -266,39 +267,48 @@ export default class YTMusicSource extends AbstractSource {

let newPlays: PlayObject[] = [];

const plays = ytiHistoryResponseToListItems(playlistDetail).map((x) => YTMusicSource.formatPlayObj(x, {newFromSource: false})).slice(0, 20);
const page = Parser.parseResponse<IBrowseResponse>(playlistDetail.data);
const shelfPlays = ytiHistoryResponseFromShelfToPlays(playlistDetail);
const listPlays = ytiHistoryResponseToListItems(playlistDetail).map((x) => YTMusicSource.formatPlayObj(x, {newFromSource: false}));
const plays = listPlays.slice(0, 20);
if(this.polling === false) {
this.recentlyPlayed = plays;
newPlays = plays;
} else {
if(playsAreSortConsistent(this.recentlyPlayed, plays)) {
return newPlays;
}
const [ok, diff, addType] = playsAreAddedOnly(this.recentlyPlayed, plays);
if(!ok || addType === 'insert' || addType === 'append') {
const playsDiff = getPlaysDiff(this.recentlyPlayed, plays)
const humanDiff = humanReadableDiff(this.recentlyPlayed, plays, playsDiff);
this.logger.warn('YTM History returned temporally inconsistent order, resetting watched history to new list.');
this.logger.warn(`Changes from last seen list:
${humanDiff}`);
this.recentlyPlayed = plays;
return newPlays;

const bumpResults = playsAreBumpedOnly(this.recentlyPlayed, plays);
if(bumpResults[0] === true) {
newPlays = bumpResults[1];
} else {
// new plays
newPlays = [...diff].reverse();
this.recentlyPlayed = plays;
const addResults = playsAreAddedOnly(this.recentlyPlayed, plays);
if(addResults[0] === true) {
newPlays = [...addResults[1]].reverse();
} else {
const playsDiff = getPlaysDiff(this.recentlyPlayed, plays)
const humanDiff = humanReadableDiff(this.recentlyPlayed, plays, playsDiff);
this.logger.warn('YTM History returned temporally inconsistent order, resetting watched history to new list.');
this.logger.warn(`Changes from last seen list:
${humanDiff}`);
this.recentlyPlayed = plays;
return newPlays;
}
}

this.recentlyPlayed = plays;

newPlays = newPlays.map((x) => ({
newPlays = newPlays.map((x, index) => ({
data: {
...x.data,
playDate: dayjs().startOf('minute')
playDate: dayjs().startOf('minute').add(index, 's')
},
meta: {
...x.meta,
newFromSource: true
}
}));
}
}

return newPlays;
Expand Down
225 changes: 188 additions & 37 deletions src/backend/tests/utils/playComparisons.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { loggerTest } from "@foxxmd/logging";
import { assert } from 'chai';
import clone from "clone";
import { describe, it } from 'mocha';
import { playsAreAddedOnly, playsAreSortConsistent } from "../../utils/PlayComparisonUtils.js";
import { playsAreAddedOnly, playsAreBumpedOnly, playsAreSortConsistent } from "../../utils/PlayComparisonUtils.js";
import { generatePlay, generatePlays } from "./PlayTestUtils.js";
import { PlayObject } from "../../../core/Atomic.js";

const logger = loggerTest;

Expand Down Expand Up @@ -33,69 +34,219 @@ describe('Compare lists by order', function () {
});
});

describe('Added Only', function () {
describe('Non-identical lists', function() {
let candidateList: PlayObject[];

it('Non-identical lists are not add only', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, generatePlays(10))
before(function() {
candidateList = generatePlays(10);
});

it('are not add only', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList)
assert.isFalse(ok);
});

it('are not bump only', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList)
assert.isFalse(ok);
});
});

it('Lists with only prepended additions are detected', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, [generatePlay(), generatePlay(), ...existingList])
describe('Lists with only prepended additions', function() {
let candidateList: PlayObject[];

before(function() {
candidateList = [generatePlay(), generatePlay(), ...existingList];
});

it('are add only', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList)
assert.isTrue(ok);
assert.equal(addType, 'prepend');
});

it('Lists with only appended additions are detected', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, [...existingList, generatePlay(), generatePlay()])
it('are not bump only', function () {
const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList)
assert.isFalse(ok);
});
});

describe('Lists with only appended additions', function() {
let candidateList: PlayObject[];

before(function() {
candidateList = [...existingList, generatePlay(), generatePlay()];
});

it('are add only', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList)
assert.isTrue(ok);
assert.equal(addType, 'append');
});

it('Lists of fixed length with prepends are correctly detected', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, [generatePlay(), generatePlay(), ...existingList].slice(0, 9))
it('are not bump only', function () {
const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList)
assert.isFalse(ok);
});

});

describe('Lists of fixed length with prepends', function() {
let candidateList: PlayObject[];

before(function() {
candidateList = [generatePlay(), generatePlay(), ...existingList].slice(0, 9);
});

it('are add only', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList)
assert.isTrue(ok);
assert.equal(addType, 'prepend');
});

it('Lists with inserts are detected', function () {
const splicedList1 = [...existingList.map(x => clone(x))];
splicedList1.splice(4, 0, generatePlay())
const [ok, diff, addType] = playsAreAddedOnly(existingList, splicedList1)
it('are not bump only', function () {
const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList)
assert.isFalse(ok);
});
});


describe('Lists with inserts', function() {
let candidateList: PlayObject[],
candidateList2: PlayObject[];

before(function() {
candidateList = [...existingList.map(x => clone(x))];
candidateList.splice(4, 0, generatePlay())

candidateList2 = [...existingList.map(x => clone(x))];
candidateList2.splice(2, 0, generatePlay())
candidateList2.splice(6, 0, generatePlay())
});

it('are not add only', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList)
assert.isFalse(ok)

const [ok2, diff2, addType2] = playsAreAddedOnly(existingList, candidateList2)
assert.isFalse(ok2)
});

it('are not bump only', function () {
const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList)
assert.isFalse(ok)
//assert.equal(addType, 'insert');

const splicedList2 = [...existingList.map(x => clone(x))];
splicedList2.splice(2, 0, generatePlay())
splicedList2.splice(6, 0, generatePlay())
const [ok2, diff2, addType2] = playsAreAddedOnly(existingList, splicedList2)
const [ok2, diff2, addType2] = playsAreBumpedOnly(existingList, candidateList2)
assert.isFalse(ok2)
//assert.equal(addType2, 'insert');
});

});

describe('Lists with inserts and prepends', function() {
let candidateList: PlayObject[];

before(function() {
candidateList = [...existingList.map(x => clone(x))];
candidateList.splice(2, 0, generatePlay())
candidateList.splice(6, 0, generatePlay())
candidateList = [generatePlay(), generatePlay(), ...candidateList]
});

it('Lists with inserts and prepends are detected as inserts', function () {
const splicedList = [...existingList.map(x => clone(x))];
splicedList.splice(2, 0, generatePlay())
splicedList.splice(6, 0, generatePlay())
const [ok, diff3, addType] = playsAreAddedOnly(existingList, [generatePlay(), generatePlay(), ...splicedList])
it('are not add only', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList)
assert.isFalse(ok);
//assert.equal(addType, 'insert');
});

it('Lists with inserts and appends are detected as inserts', function () {
const splicedList = [...existingList.map(x => clone(x))];
splicedList.splice(2, 0, generatePlay())
splicedList.splice(6, 0, generatePlay())
const [ok, diff4, addType] = playsAreAddedOnly(existingList, [...splicedList, generatePlay(), generatePlay()])
it('are not bump only', function () {
const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList)
assert.isFalse(ok);
//assert.equal(addType, 'insert');
});

});

describe('Lists with inserts and appends', function() {
let candidateList: PlayObject[];

before(function() {
candidateList = [...existingList.map(x => clone(x))];
candidateList.splice(2, 0, generatePlay())
candidateList.splice(6, 0, generatePlay())
candidateList = [...candidateList, generatePlay(), generatePlay()]
});

it('are not add only', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList)
assert.isFalse(ok);
});

it('are not bump only', function () {
const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList)
assert.isFalse(ok);
});
});

describe('Lists with inserts and appends and prepends', function() {
let candidateList: PlayObject[];

before(function() {
candidateList = [...existingList.map(x => clone(x))];
candidateList = [generatePlay(), generatePlay(), ...candidateList, generatePlay(), generatePlay()]
});

it('are not add only', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList)
assert.isFalse(ok);
});

it('are not bump only', function () {
const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList)
assert.isFalse(ok);
});
});

describe('Lists with plays bumped-by-prepend', function() {
let candidateList: PlayObject[];

before(function() {
candidateList = [...existingList.map(x => clone(x))];
const bumped = candidateList[6];
candidateList.splice(6, 1);
candidateList.unshift(bumped);
});

it('are not add only', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList);
assert.isFalse(ok);
});

it('are bump only', function () {
const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList);
assert.isTrue(ok);
assert.equal(addType, 'prepend');
});

});

describe('Lists with plays bumped-by-append', function() {
let candidateList: PlayObject[];

before(function() {
candidateList = [...existingList.map(x => clone(x))];
const bumped = candidateList[6];
candidateList.splice(6, 1);
candidateList.push(bumped);
});

it('Lists with inserts and appends and prepends are detected as inserts', function () {
const splicedList = [...existingList.map(x => clone(x))];
const [ok, diff, addType] = playsAreAddedOnly(existingList, [generatePlay(), generatePlay(), ...splicedList, generatePlay(), generatePlay()])
it('are not add only', function () {
const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList);
assert.isFalse(ok);
//assert.equal(addType, 'insert');
});
})

it('are bump only', function () {
const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList);
assert.isTrue(ok);
assert.equal(addType, 'append');
});

});
});
Loading

0 comments on commit 0ec701b

Please sign in to comment.