Created
February 9, 2023 22:49
-
-
Save snuffyDev/c22ecf860aa2160c1e04841f6908a9fe to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| }); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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