Skip to content

Instantly share code, notes, and snippets.

@snuffyDev
Created February 9, 2023 22:49
Show Gist options
  • Select an option

  • Save snuffyDev/c22ecf860aa2160c1e04841f6908a9fe to your computer and use it in GitHub Desktop.

Select an option

Save snuffyDev/c22ecf860aa2160c1e04841f6908a9fe to your computer and use it in GitHub Desktop.
type MixListAppendOp = [op: "append" | "set", data: ISessionListProvider["mix"]];
class ListService implements ISessionListService {
private $: Writable<ISessionListProvider>;
private state: ISessionListProvider = {
clickTrackingParams: "",
continuation: "",
currentMixId: "",
currentMixType: null,
visitorData: "",
mix: [],
position: 0,
};
constructor() {
this.$ = writable<ISessionListProvider>(this.state);
}
public get set() {
return this.$.set;
}
public get subscribe() {
return this.$.subscribe;
}
private get update() {
return this.$.update;
}
public async initAutoMixSession(args: AutoMixArgs) {
const { loggingContext, keyId, clickTracking, config, playlistId, playlistSetVideoId, videoId } = args;
// Wait ffor the DOM to update
let willRevert = false;
// Reset the current mix state
if (this.state.mix) {
willRevert = true;
this.revertState();
}
this.state.currentMixType = "auto";
const data = await fetchNext({
params: config?.playerParams ? config?.playerParams : undefined,
videoId,
playlistId: playlistId ? playlistId : undefined,
loggingContext: loggingContext ? loggingContext.vssLoggingContext?.serializedContextData : undefined,
playlistSetVideoId: playlistSetVideoId ? playlistSetVideoId : undefined,
clickTracking,
configType: config?.type || undefined,
});
if (!data || !Array.isArray(data.results)) {
throw new Error("Invalid response was returned from `next` endpoint.");
}
const item = data.results[keyId ?? 0];
getSrc(videoId ?? item?.videoId, item?.playlistId, config?.playerParams);
// Pull out results from the data, so we can apply the rest separately
const { results } = data;
delete data.results;
this.sanitizeAndUpdate(
willRevert ? "SET" : "APPLY",
Object.assign(this.state, { ...data, mix: ["append", results] }),
);
if (groupSession?.initialized && groupSession?.hasActiveSession) {
groupSession.expAutoMix(this.state);
}
}
/** Update the track position based on a keyword or number */
public updatePosition(direction: "next" | "back" | number): number {
if (typeof direction === "number") {
this.sanitizeAndUpdate("APPLY", { position: direction });
return direction;
}
if (direction === "next") {
this.sanitizeAndUpdate("APPLY", { position: this.state.position + 1 });
}
if (direction === "back") {
this.sanitizeAndUpdate("APPLY", { position: this.state.position - 1 });
}
return this.state.position;
}
private revertState(): ISessionListProvider {
this.state = {
clickTrackingParams: "",
continuation: "",
currentMixId: "",
currentMixType: null,
mix: [],
visitorData: "",
position: 0,
};
return this.state;
}
/** Sanitize (diff) and update the state */
private sanitizeAndUpdate(
kind: "APPLY" | "SET",
to: {
[Key in keyof ISessionListProvider]?: ISessionListProvider[Key] extends any[]
? MixListAppendOp | Item[]
: ISessionListProvider[Key];
},
) {
this.update((old) => {
if (kind === "APPLY") {
const toKeys = Object.keys(to);
// Get the `to` keys that are populated (non-null, not undefined).
const toKeysPopulated = toKeys.filter((u) => to[u] !== undefined && to[u] !== null);
let key;
for (let idx = 0; idx < toKeysPopulated.length; idx++) {
key = toKeysPopulated[idx];
// Skip if same value
if (old[key] === to[key]) continue;
// `mix` has a slightly altered type here
if (key === "mix") {
// index 0 = operation
// index 1 = data
if (to.mix[0] === "append") {
old.mix.push(...(to.mix as MixListAppendOp)[1]);
} else if (to.mix[0] === "set") {
old.mix = (to.mix as MixListAppendOp)[1];
}
} else if (to[key] !== undefined && to[key] !== null) {
old[key] = to[key];
}
}
this.state = Object.assign(this.state, old);
return { ...old, ...to } as ISessionListProvider;
} else {
const { mix } = to;
return { ...to, mix: mix[1] ? mix[1] : old["mix"] } as ISessionListProvider;
}
});
}
}
/**
* TODO: clean this module up massively.
*
* - Use class instead of func impl (...?)
*
*/
import type { Item, Nullable } from "$lib/types";
import { WritableStore, addToQueue, getSrc, notify, seededShuffle } from "$lib/utils";
import { Mutex } from "$lib/utils/sync";
import { splice } from "$lib/utils/collections/array";
import { writable, get, type Writable } from "svelte/store";
import { playerLoading, currentTitle, filterAutoPlay } from "../stores";
import { groupSession } from "../sessions";
import type { ISessionListService, ISessionListProvider } from "./types.list";
import { fetchNext, filterList } from "./utils.list";
const mutex = new Mutex();
const SessionListService: ISessionListService = _sessionListService();
interface AutoMixArgs {
videoId?: string;
playlistId?: string;
keyId?: number;
playlistSetVideoId?: string;
loggingContext: { vssLoggingContext: { serializedContextData: string } };
clickTracking?: string;
config?: { playerParams?: string; type?: string };
}
function togglePlayerLoad() {
playerLoading.set(true);
return () => playerLoading.set(false);
}
// class CSessionListService implements ISessionListService {
// private $$store: Writable<ISessionListProvider>;
// private _visitorData = "";
// private _data: ISessionListProvider = {
// clickTrackingParams: "",
// continuation: "",
// currentMixId: "",
// currentMixType: "",
// mix: [],
// position: 0,
// };
// constructor() {
// this.$$store = writable<ISessionListProvider>(this._data);
// }
// private get update() {
// return this.$$store.update;
// }
// public get subscribe() {
// return this.$$store.subscribe;
// }
// }
function _sessionListService(): ISessionListService {
// default values for the store
let mix: Item[] = [],
continuation = "",
clickTrackingParams: Nullable<string> = "",
currentMixId = "",
position = 0,
currentMixType: "playlist" | "auto" | string = "",
related = "";
let visitorData = "";
const { update, subscribe } = writable<ISessionListProvider>({
mix,
currentMixId,
clickTrackingParams,
continuation,
position,
currentMixType,
});
// Used when playlist session is initialized with more than 50 items
let chunkedListOriginalLen: number;
let chunkedPlaylistCurrentIdx = 0;
const chunkedPlaylistMap = new Map<number, Item[]>();
const _set = (value: ISessionListProvider) => {
clickTrackingParams = value.clickTrackingParams ?? clickTrackingParams;
continuation = value.continuation ?? continuation;
currentMixId = value.currentMixId ?? currentMixId;
mix = value.mix ? value.mix : mix;
position = value.position ?? position;
currentMixType = value.currentMixType ?? currentMixType;
update((_) => ({ ..._, mix, position, currentMixId, continuation, clickTrackingParams, currentMixType }));
return {
clickTrackingParams,
continuation,
currentMixId,
mix,
position,
currentMixType,
};
};
const commitChanges = ({
clickTrackingParams,
mix,
continuation,
currentMixId,
position,
currentMixType,
}: ISessionListProvider) => _set({ clickTrackingParams, mix, continuation, currentMixId, position, currentMixType });
async function getMoreLikeThis({ playlistId }: { playlistId: Nullable<string> }) {
if (!mix.length) {
return;
}
playerLoading.set(true);
const response = await fetchNext({
params: "wAEB8gECeAE%3D",
playlistId: "RDAMPL" + (playlistId !== null ? playlistId : currentMixId),
});
const data = await response;
data.results.shift();
mix.push(...data.results);
continuation = data.continuation;
commitChanges({ mix, clickTrackingParams, currentMixId, continuation, position, currentMixType });
if (groupSession?.initialized && groupSession?.hasActiveSession) {
groupSession.updateGuestTrackQueue({
mix,
clickTrackingParams,
currentMixId,
continuation,
position,
currentMixType,
});
}
playerLoading.set(false);
}
return {
subscribe,
set: _set,
async lockedSet(_mix: ISessionListProvider) {
return mutex.do(() => {
return _set(_mix);
});
},
async initAutoMixSession({
clickTracking,
keyId = 0,
playlistId,
playlistSetVideoId,
loggingContext = null,
videoId,
config: { playerParams = "", type = "" } = {},
}) {
try {
playerLoading.set(true);
if (mix.length > 0) {
mix = [];
clickTrackingParams = null;
}
currentMixType = "auto";
const data = await fetchNext({
params: playerParams ? playerParams : "",
videoId,
playlistId: playlistId ? playlistId : "",
loggingContext: loggingContext ? loggingContext.vssLoggingContext.serializedContextData : undefined,
playlistSetVideoId: playlistSetVideoId ? playlistSetVideoId : "",
clickTracking,
configType: type,
});
if (!data || !Array.isArray(data["results"])) throw new Error("No results!");
getSrc(videoId ?? data.results[keyId ?? 0].videoId, playlistId, playerParams);
visitorData = data["visitorData"];
currentTitle.set((Array.isArray(data.results) && data.results[keyId ?? 0]?.title) ?? undefined);
position = keyId ?? 0;
playerLoading.set(false);
continuation = data.continuation && data.continuation.length !== 0 && data.continuation;
currentMixId = data.currentMixId;
clickTrackingParams =
data.clickTrackingParams && data.clickTrackingParams.length !== 0 && data.clickTrackingParams;
mix.push(...data.results);
commitChanges({ mix, clickTrackingParams, currentMixId, continuation, position, currentMixType });
if (groupSession?.initialized && groupSession?.hasActiveSession) {
groupSession.expAutoMix({ mix, clickTrackingParams, currentMixId, continuation, position, currentMixType });
}
} catch (err) {
playerLoading.set(false);
console.error(err);
}
},
async initPlaylistSession(args) {
let {
playlistId = "",
index = 0,
clickTrackingParams = "",
params = "",
videoId = "",
playlistSetVideoId = "",
visitorData = "",
} = args;
playerLoading.set(true);
if (currentMixType !== "playlist" || currentMixId !== playlistId) {
position = typeof index === "number" ? index : 0;
}
if (currentMixId !== playlistId) mix = [];
currentMixType = "playlist";
try {
playlistId = playlistId.startsWith("VL") ? playlistId.slice(2) : playlistId;
const data = await fetchNext({
params,
playlistId: playlistId,
clickTracking: clickTrackingParams,
visitorData,
playlistSetVideoId: playlistSetVideoId,
videoId,
});
mix.push(...data.results);
mix = filterList(mix);
playerLoading.set(false);
continuation = data?.continuation;
clickTrackingParams = data?.clickTrackingParams;
currentMixId = data?.currentMixId;
commitChanges({ mix, clickTrackingParams, currentMixId, continuation, position: index, currentMixType });
if (groupSession?.initialized && groupSession?.hasActiveSession) {
groupSession.expAutoMix({ mix, clickTrackingParams, currentMixId, continuation, position, currentMixType });
}
return await getSrc(mix[index].videoId, playlistId);
} catch (err) {
console.error(err);
playerLoading.set(false);
notify("Error starting playback", "error");
return null;
}
},
async setMix(mix: Item[], type?: "auto" | "playlist" | "local") {
const guard = await mutex.do(async () => {
return new Promise<ISessionListProvider>((resolve) => {
resolve(
commitChanges({
mix,
clickTrackingParams,
currentMixId,
continuation,
position,
currentMixType: type ?? currentMixType,
}),
);
});
});
if (groupSession?.initialized && groupSession?.hasActiveSession) {
groupSession.send("PUT", "state.set.mix", JSON.stringify(guard), groupSession.client);
}
},
getMoreLikeThis,
async getSessionContinuation({ clickTrackingParams, ctoken, itct, key, playlistId, videoId, loggingContext }) {
playerLoading.set(true);
if (currentMixType === "playlist" && chunkedPlaylistMap.size && mix.length < chunkedListOriginalLen - 1) {
chunkedPlaylistCurrentIdx++;
const src = await getSrc(mix[mix.length - 1].videoId);
mix.push(...Array.from(chunkedPlaylistMap.get(chunkedPlaylistCurrentIdx)!));
mix = get(filterAutoPlay) ? [...filterList(mix)] : [...mix];
playerLoading.set(false);
commitChanges({ mix, clickTrackingParams, currentMixId, continuation, position, currentMixType });
return await src.body;
}
if (!clickTrackingParams && !ctoken) {
playlistId = "RDAMPL" + playlistId;
itct = "wAEB8gECeAE%3D";
}
const data = await fetchNext({
visitorData: visitorData,
params: encodeURIComponent("OAHyAQIIAQ=="),
playlistSetVideoId: mix[position]?.playlistSetVideoId,
index: mix.length,
videoId,
playlistId,
ctoken,
clickTracking: clickTrackingParams,
}).then((res) => {
if (res.results.length === 0) getMoreLikeThis({ playlistId });
return res;
});
const results = data?.results as any[];
mix.push(...results);
if (get(filterAutoPlay)) mix = filterList(mix);
visitorData = data["visitorData"] ?? visitorData;
continuation = data.continuation;
currentMixId = data.currentMixId;
clickTrackingParams = data.clickTrackingParams;
commitChanges({ mix, clickTrackingParams, currentMixId, continuation, position, currentMixType });
playerLoading.set(false);
const src = await getSrc(mix[key].videoId);
if (groupSession?.initialized && groupSession?.hasActiveSession) {
groupSession.updateGuestContinuation({
mix,
clickTrackingParams,
currentMixId,
continuation,
position,
currentMixType,
});
}
return src.body;
},
removeTrack(index: number) {
mix.splice(index, 1);
commitChanges({ mix, clickTrackingParams, currentMixId, continuation, position, currentMixType });
},
async setTrackWillPlayNext(item: Item, key) {
if (!item) {
notify("No track to remove was provided!", "error");
return;
}
try {
const itemToAdd = await addToQueue(item);
const oldLength = mix.length;
// eslint-disable-next-line no-self-assign
splice(mix, key + 1, 0, ...itemToAdd);
commitChanges({ mix, clickTrackingParams, currentMixId, continuation, position, currentMixType });
console.log({ oldLength, mix, itemToAdd });
if (!oldLength) {
await getSrc(mix[0].videoId, mix[0].playlistId, null, true);
}
} catch (err) {
console.error(err);
notify(`Error: ${err}`, "error");
}
},
shuffleRandom(items = []) {
mix = seededShuffle(
items,
crypto.getRandomValues(new Uint8Array(8)).reduce((prev, cur) => (prev += cur), 0),
);
commitChanges({ mix, clickTrackingParams, currentMixId, continuation, position, currentMixType });
if (groupSession?.initialized && groupSession?.hasActiveSession) {
groupSession.updateGuestTrackQueue({
mix,
clickTrackingParams,
currentMixId,
continuation,
position,
currentMixType,
});
}
},
shuffle(index: number, preserveBeforeActive = true) {
if (typeof index !== "number") return;
if (!preserveBeforeActive) {
mix = seededShuffle(
mix.slice(),
crypto.getRandomValues(new Uint8Array(8)).reduce((prev, cur) => (prev += cur), 0),
);
} else {
mix = [
...mix.slice(0, index),
mix[index],
...seededShuffle(
mix.slice(index + 1),
crypto.getRandomValues(new Uint8Array(8)).reduce((prev, cur) => (prev += cur), 0),
),
];
}
// console.log(mix)
commitChanges({ mix, clickTrackingParams, currentMixId, continuation, position, currentMixType });
if (groupSession?.initialized && groupSession?.hasActiveSession) {
groupSession.updateGuestTrackQueue({
mix,
clickTrackingParams,
currentMixId,
continuation,
position,
currentMixType,
});
}
},
toJSON(): string {
return JSON.stringify({ clickTrackingParams, continuation, currentMixId, mix, position, currentMixType });
},
get mix() {
return mix;
},
get position() {
return position;
},
get clickTrackingParams() {
return clickTrackingParams ?? "";
},
get continuation() {
return continuation;
},
get currentMixId() {
return currentMixId;
},
updatePosition(direction: "next" | "back" | number): number {
if (typeof direction === "number") {
position = direction;
commitChanges({ mix, clickTrackingParams, currentMixId, continuation, position, currentMixType });
return position;
}
if (direction === "next") {
position++;
}
if (direction === "back") {
position--;
}
commitChanges({ mix, clickTrackingParams, currentMixId, continuation, position, currentMixType });
return position;
},
};
}
export default SessionListService;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment