fix: apply fix from eslint

This commit is contained in:
JellyBrick
2024-10-13 22:45:11 +09:00
parent f42f20f770
commit cb1381bbb3
85 changed files with 1858 additions and 1042 deletions

View File

@ -3,13 +3,15 @@ import { DataConnection, Peer } from 'peerjs';
import type { Permission, Profile, VideoData } from './types';
export type ConnectionEventMap = {
ADD_SONGS: { videoList: VideoData[], index?: number };
ADD_SONGS: { videoList: VideoData[]; index?: number };
REMOVE_SONG: { index: number };
MOVE_SONG: { fromIndex: number; toIndex: number };
IDENTIFY: { profile: Profile } | undefined;
SYNC_PROFILE: { profiles: Record<string, Profile> } | undefined;
SYNC_QUEUE: { videoList: VideoData[] } | undefined;
SYNC_PROGRESS: { progress?: number; state?: number; index?: number; } | undefined;
SYNC_PROGRESS:
| { progress?: number; state?: number; index?: number }
| undefined;
PERMISSION: Permission | undefined;
};
export type ConnectionEventUnion = {
@ -24,9 +26,12 @@ type PromiseUtil<T> = {
promise: Promise<T>;
resolve: (id: T) => void;
reject: (err: unknown) => void;
}
};
export type ConnectionListener = (event: ConnectionEventUnion, conn: DataConnection) => void;
export type ConnectionListener = (
event: ConnectionEventUnion,
conn: DataConnection,
) => void;
export type ConnectionMode = 'host' | 'guest' | 'disconnected';
export class Connection {
private peer: Peer;
@ -95,9 +100,12 @@ export class Connection {
return Object.values(this.connections);
}
public async broadcast<Event extends keyof ConnectionEventMap>(type: Event, payload: ConnectionEventMap[Event]) {
public async broadcast<Event extends keyof ConnectionEventMap>(
type: Event,
payload: ConnectionEventMap[Event],
) {
await Promise.all(
this.getConnections().map((conn) => conn.send({ type, payload }))
this.getConnections().map((conn) => conn.send({ type, payload })),
);
}
@ -125,7 +133,13 @@ export class Connection {
this.connectionListeners.forEach((listener) => listener(conn));
conn.on('data', (data) => {
if (!data || typeof data !== 'object' || !('type' in data) || !('payload' in data) || !data.type) {
if (
!data ||
typeof data !== 'object' ||
!('type' in data) ||
!('payload' in data) ||
!data.type
) {
console.warn('Music Together: Invalid data', data);
return;
}

View File

@ -4,7 +4,7 @@ import itemHTML from './templates/item.html?raw';
import popupHTML from './templates/popup.html?raw';
type Placement =
'top'
| 'top'
| 'bottom'
| 'right'
| 'left'
@ -15,32 +15,40 @@ type Placement =
| 'top-right'
| 'bottom-left'
| 'bottom-right';
type PopupItem = (ItemRendererProps & { type: 'item'; })
| { type: 'divider'; }
| { type: 'custom'; element: HTMLElement; };
type PopupItem =
| (ItemRendererProps & { type: 'item' })
| { type: 'divider' }
| { type: 'custom'; element: HTMLElement };
type PopupProps = {
data: PopupItem[];
anchorAt?: Placement;
popupAt?: Placement;
}
};
export const Popup = (props: PopupProps) => {
const popup = ElementFromHtml(popupHTML);
const container = popup.querySelector<HTMLElement>('.music-together-popup-container')!;
const container = popup.querySelector<HTMLElement>(
'.music-together-popup-container',
)!;
const items = props.data
.map((props) => {
if (props.type === 'item') return {
type: 'item' as const,
...ItemRenderer(props),
};
if (props.type === 'divider') return {
type: 'divider' as const,
element: ElementFromHtml('<div class="music-together-divider horizontal"></div>'),
};
if (props.type === 'custom') return {
type: 'custom' as const,
element: props.element,
};
if (props.type === 'item')
return {
type: 'item' as const,
...ItemRenderer(props),
};
if (props.type === 'divider')
return {
type: 'divider' as const,
element: ElementFromHtml(
'<div class="music-together-divider horizontal"></div>',
),
};
if (props.type === 'custom')
return {
type: 'custom' as const,
element: props.element,
};
return null;
})
@ -80,7 +88,9 @@ export const Popup = (props: PopupProps) => {
setTimeout(() => {
const onClose = (event: MouseEvent) => {
const isPopupClick = event.composedPath().some((element) => element === popup);
const isPopupClick = event
.composedPath()
.some((element) => element === popup);
if (!isPopupClick) {
this.dismiss();
document.removeEventListener('click', onClose);
@ -101,7 +111,7 @@ export const Popup = (props: PopupProps) => {
dismiss() {
popup.style.setProperty('opacity', '0');
popup.style.setProperty('pointer-events', 'none');
}
},
};
};
@ -133,6 +143,6 @@ export const ItemRenderer = (props: ItemRendererProps) => {
setText(text: string) {
textContainer.replaceChildren(text);
},
id: props.id
id: props.id,
};
};

View File

@ -6,7 +6,12 @@ import { t } from '@/i18n';
import { createPlugin } from '@/utils';
import promptOptions from '@/providers/prompt-options';
import { getDefaultProfile, type Permission, type Profile, type VideoData } from './types';
import {
getDefaultProfile,
type Permission,
type Profile,
type VideoData,
} from './types';
import { Queue } from './queue';
import { Connection, type ConnectionEventUnion } from './connection';
import { createHostPopup } from './ui/host';
@ -26,7 +31,7 @@ type RawAccountData = {
runs: { text: string }[];
};
accountPhoto: {
thumbnails: { url: string; width: number; height: number; }[];
thumbnails: { url: string; width: number; height: number }[];
};
settingsEndpoint: unknown;
manageAccountTitle: unknown;
@ -59,7 +64,7 @@ export default createPlugin<
stateInterval?: number;
updateNext: boolean;
ignoreChange: boolean;
rollbackInjector?: (() => void);
rollbackInjector?: () => void;
me?: Omit<Profile, 'id'>;
profiles: Record<string, Profile>;
permission: Permission;
@ -79,16 +84,18 @@ export default createPlugin<
restartNeeded: false,
addedVersion: '3.2.X',
config: {
enabled: false
enabled: false,
},
stylesheets: [style],
backend({ ipc }) {
ipc.handle('music-together:prompt', async (title: string, label: string) => prompt({
title,
label,
type: 'input',
...promptOptions()
}));
ipc.handle('music-together:prompt', async (title: string, label: string) =>
prompt({
title,
label,
type: 'input',
...promptOptions(),
}),
);
},
renderer: {
updateNext: false,
@ -112,15 +119,19 @@ export default createPlugin<
videoChangeListener(event: CustomEvent<VideoDataChanged>) {
if (event.detail.name === 'dataloaded' || this.updateNext) {
if (this.connection?.mode === 'host') {
const videoList: VideoData[] = this.queue?.flatItems.map((it) => ({
videoId: it!.videoId,
ownerId: this.connection!.id
} satisfies VideoData)) ?? [];
const videoList: VideoData[] =
this.queue?.flatItems.map(
(it) =>
({
videoId: it!.videoId,
ownerId: this.connection!.id,
}) satisfies VideoData,
) ?? [];
this.queue?.setVideoList(videoList, false);
this.queue?.syncQueueOwner();
this.connection.broadcast('SYNC_QUEUE', {
videoList
videoList,
});
this.updateNext = event.detail.name === 'dataloaded';
@ -138,7 +149,7 @@ export default createPlugin<
this.connection.broadcast('SYNC_PROGRESS', {
// progress: this.playerApi?.getCurrentTime(),
state: this.playerApi?.getPlayerState()
state: this.playerApi?.getPlayerState(),
// index: this.queue?.selectedIndex ?? 0,
});
},
@ -150,13 +161,17 @@ export default createPlugin<
if (!wait) return false;
if (!this.me) this.me = getDefaultProfile(this.connection.id);
const rawItems = this.queue?.flatItems?.map((it) => ({
videoId: it!.videoId,
ownerId: this.connection!.id
} satisfies VideoData)) ?? [];
const rawItems =
this.queue?.flatItems?.map(
(it) =>
({
videoId: it!.videoId,
ownerId: this.connection!.id,
}) satisfies VideoData,
) ?? [];
this.queue?.setOwner({
id: this.connection.id,
...this.me
...this.me,
});
this.queue?.setVideoList(rawItems, false);
this.queue?.syncQueueOwner();
@ -166,31 +181,41 @@ export default createPlugin<
this.profiles = {};
this.connection.onConnections((connection) => {
if (!connection) {
this.api?.toastService?.show(t('plugins.music-together.toast.disconnected'));
this.api?.toastService?.show(
t('plugins.music-together.toast.disconnected'),
);
this.onStop();
return;
}
if (!connection.open) {
this.api?.toastService?.show(t('plugins.music-together.toast.user-disconnected', {
name: this.profiles[connection.peer]?.name
}));
this.api?.toastService?.show(
t('plugins.music-together.toast.user-disconnected', {
name: this.profiles[connection.peer]?.name,
}),
);
this.putProfile(connection.peer, undefined);
}
});
this.putProfile(this.connection.id, {
id: this.connection.id,
...this.me
...this.me,
});
const listener = async (event: ConnectionEventUnion, conn?: DataConnection) => {
const listener = async (
event: ConnectionEventUnion,
conn?: DataConnection,
) => {
this.ignoreChange = true;
switch (event.type) {
case 'ADD_SONGS': {
if (conn && this.permission === 'host-only') return;
await this.queue?.addVideos(event.payload.videoList, event.payload.index);
await this.queue?.addVideos(
event.payload.videoList,
event.payload.index,
);
await this.connection?.broadcast('ADD_SONGS', event.payload);
break;
}
@ -204,27 +229,38 @@ export default createPlugin<
case 'MOVE_SONG': {
if (conn && this.permission === 'host-only') {
await this.connection?.broadcast('SYNC_QUEUE', {
videoList: this.queue?.videoList ?? []
videoList: this.queue?.videoList ?? [],
});
break;
}
this.queue?.moveItem(event.payload.fromIndex, event.payload.toIndex);
this.queue?.moveItem(
event.payload.fromIndex,
event.payload.toIndex,
);
await this.connection?.broadcast('MOVE_SONG', event.payload);
break;
}
case 'IDENTIFY': {
if (!event.payload || !conn) {
console.warn('Music Together [Host]: Received "IDENTIFY" event without payload or connection');
console.warn(
'Music Together [Host]: Received "IDENTIFY" event without payload or connection',
);
break;
}
this.api?.toastService?.show(t('plugins.music-together.toast.user-connected', { name: event.payload.profile.name }));
this.api?.toastService?.show(
t('plugins.music-together.toast.user-connected', {
name: event.payload.profile.name,
}),
);
this.putProfile(conn.peer, event.payload.profile);
break;
}
case 'SYNC_PROFILE': {
await this.connection?.broadcast('SYNC_PROFILE', { profiles: this.profiles });
await this.connection?.broadcast('SYNC_PROFILE', {
profiles: this.profiles,
});
break;
}
@ -237,7 +273,7 @@ export default createPlugin<
}
case 'SYNC_QUEUE': {
await this.connection?.broadcast('SYNC_QUEUE', {
videoList: this.queue?.videoList ?? []
videoList: this.queue?.videoList ?? [],
});
break;
}
@ -251,7 +287,8 @@ export default createPlugin<
if (permissionLevel >= 2) {
if (typeof event.payload?.progress === 'number') {
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
if (Math.abs(event.payload.progress - currentTime) > 3) this.playerApi?.seekTo(event.payload.progress);
if (Math.abs(event.payload.progress - currentTime) > 3)
this.playerApi?.seekTo(event.payload.progress);
}
if (this.playerApi?.getPlayerState() !== event.payload?.state) {
if (event.payload?.state === 2) this.playerApi?.pauseVideo();
@ -300,25 +337,32 @@ export default createPlugin<
this.profiles = {};
const id = await this.showPrompt(t('plugins.music-together.name'), t('plugins.music-together.dialog.enter-host'));
const id = await this.showPrompt(
t('plugins.music-together.name'),
t('plugins.music-together.dialog.enter-host'),
);
if (typeof id !== 'string') return false;
const connection = await this.connection.connect(id).catch(() => false);
if (!connection) return false;
this.connection.onConnections((connection) => {
if (!connection?.open) {
this.api?.toastService?.show(t('plugins.music-together.toast.disconnected'));
this.api?.toastService?.show(
t('plugins.music-together.toast.disconnected'),
);
this.onStop();
}
});
let resolveIgnore: number | null = null;
const listener = async (event: ConnectionEventUnion) => {
this.ignoreChange = true;
switch (event.type) {
case 'ADD_SONGS': {
await this.queue?.addVideos(event.payload.videoList, event.payload.index);
await this.queue?.addVideos(
event.payload.videoList,
event.payload.index,
);
break;
}
case 'REMOVE_SONG': {
@ -326,11 +370,16 @@ export default createPlugin<
break;
}
case 'MOVE_SONG': {
this.queue?.moveItem(event.payload.fromIndex, event.payload.toIndex);
this.queue?.moveItem(
event.payload.fromIndex,
event.payload.toIndex,
);
break;
}
case 'IDENTIFY': {
console.warn('Music Together [Guest]: Received "IDENTIFY" event from guest');
console.warn(
'Music Together [Guest]: Received "IDENTIFY" event from guest',
);
break;
}
case 'SYNC_QUEUE': {
@ -341,7 +390,9 @@ export default createPlugin<
}
case 'SYNC_PROFILE': {
if (!event.payload) {
console.warn('Music Together [Guest]: Received "SYNC_PROFILE" event without payload');
console.warn(
'Music Together [Guest]: Received "SYNC_PROFILE" event without payload',
);
break;
}
@ -353,7 +404,8 @@ export default createPlugin<
case 'SYNC_PROGRESS': {
if (typeof event.payload?.progress === 'number') {
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
if (Math.abs(event.payload.progress - currentTime) > 3) this.playerApi?.seekTo(event.payload.progress);
if (Math.abs(event.payload.progress - currentTime) > 3)
this.playerApi?.seekTo(event.payload.progress);
}
if (this.playerApi?.getPlayerState() !== event.payload?.state) {
if (event.payload?.state === 2) this.playerApi?.pauseVideo();
@ -370,7 +422,9 @@ export default createPlugin<
}
case 'PERMISSION': {
if (!event.payload) {
console.warn('Music Together [Guest]: Received "PERMISSION" event without payload');
console.warn(
'Music Together [Guest]: Received "PERMISSION" event without payload',
);
break;
}
@ -379,9 +433,15 @@ export default createPlugin<
this.popups.host.setPermission(this.permission);
this.popups.setting.setPermission(this.permission);
const permissionLabel = t(`plugins.music-together.menu.permission.${this.permission}`);
const permissionLabel = t(
`plugins.music-together.menu.permission.${this.permission}`,
);
this.api?.toastService?.show(t('plugins.music-together.toast.permission-changed', { permission: permissionLabel }));
this.api?.toastService?.show(
t('plugins.music-together.toast.permission-changed', {
permission: permissionLabel,
}),
);
break;
}
default: {
@ -415,8 +475,10 @@ export default createPlugin<
break;
}
case 'SYNC_PROGRESS': {
if (this.permission === 'host-only') await this.connection?.broadcast('SYNC_QUEUE', undefined);
else await this.connection?.broadcast('SYNC_PROGRESS', event.payload);
if (this.permission === 'host-only')
await this.connection?.broadcast('SYNC_QUEUE', undefined);
else
await this.connection?.broadcast('SYNC_PROGRESS', event.payload);
break;
}
}
@ -431,12 +493,16 @@ export default createPlugin<
this.queue?.injection();
this.queue?.setOwner({
id: this.connection.id,
...this.me
...this.me,
});
const progress = Array.from(document.querySelectorAll<HTMLElement & {
_update: (...args: unknown[]) => void
}>('tp-yt-paper-progress'));
const progress = Array.from(
document.querySelectorAll<
HTMLElement & {
_update: (...args: unknown[]) => void;
}
>('tp-yt-paper-progress'),
);
const rollbackList = progress.map((progress) => {
const original = progress._update;
progress._update = (...args) => {
@ -444,10 +510,11 @@ export default createPlugin<
if (this.permission === 'all' && typeof now === 'number') {
const currentTime = this.playerApi?.getCurrentTime() ?? 0;
if (Math.abs(now - currentTime) > 3) this.connection?.broadcast('SYNC_PROGRESS', {
progress: now,
state: this.playerApi?.getPlayerState()
});
if (Math.abs(now - currentTime) > 3)
this.connection?.broadcast('SYNC_PROGRESS', {
progress: now,
state: this.playerApi?.getPlayerState(),
});
}
original.call(progress, ...args);
@ -466,8 +533,8 @@ export default createPlugin<
id: this.connection.id,
handleId: this.me.handleId,
name: this.me.name,
thumbnail: this.me.thumbnail
}
thumbnail: this.me.thumbnail,
},
});
this.connection.broadcast('SYNC_PROFILE', undefined);
@ -525,14 +592,18 @@ export default createPlugin<
},
initMyProfile() {
const accountButton = document.querySelector<HTMLElement & {
onButtonTap: () => void
}>('ytmusic-settings-button');
const accountButton = document.querySelector<
HTMLElement & {
onButtonTap: () => void;
}
>('ytmusic-settings-button');
accountButton?.onButtonTap();
setTimeout(() => {
accountButton?.onButtonTap();
const renderer = document.querySelector<HTMLElement & { data: unknown }>('ytd-active-account-header-renderer');
const renderer = document.querySelector<
HTMLElement & { data: unknown }
>('ytd-active-account-header-renderer');
if (!accountButton || !renderer) {
console.warn('Music Together: Cannot find account');
this.me = getDefaultProfile(this.connection?.id ?? '');
@ -543,7 +614,7 @@ export default createPlugin<
this.me = {
handleId: accountData.channelHandle.runs[0].text,
name: accountData.accountName.runs[0].text,
thumbnail: accountData.accountPhoto.thumbnails[0].url
thumbnail: accountData.accountPhoto.thumbnails[0].url,
};
if (this.me.thumbnail) {
@ -557,14 +628,23 @@ export default createPlugin<
start({ ipc }) {
this.ipc = ipc;
this.showPrompt = async (title: string, label: string) => ipc.invoke('music-together:prompt', title, label) as Promise<string>;
this.showPrompt = async (title: string, label: string) =>
ipc.invoke('music-together:prompt', title, label) as Promise<string>;
this.api = document.querySelector<AppElement>('ytmusic-app');
/* setup */
document.querySelector('#right-content > ytmusic-settings-button')?.insertAdjacentHTML('beforebegin', settingHTML);
const setting = document.querySelector<HTMLElement>('#music-together-setting-button');
const icon = document.querySelector<SVGElement>('#music-together-setting-button > svg');
const spinner = document.querySelector<HTMLElement>('#music-together-setting-button > tp-yt-paper-spinner-lite');
document
.querySelector('#right-content > ytmusic-settings-button')
?.insertAdjacentHTML('beforebegin', settingHTML);
const setting = document.querySelector<HTMLElement>(
'#music-together-setting-button',
);
const icon = document.querySelector<SVGElement>(
'#music-together-setting-button > svg',
);
const spinner = document.querySelector<HTMLElement>(
'#music-together-setting-button > tp-yt-paper-spinner-lite',
);
if (!setting || !icon || !spinner) {
console.warn('Music Together: Cannot inject html');
console.log(setting, icon, spinner);
@ -574,7 +654,7 @@ export default createPlugin<
this.elements = {
setting,
icon,
spinner
spinner,
};
this.stateInterval = window.setInterval(() => {
@ -584,7 +664,7 @@ export default createPlugin<
this.connection.broadcast('SYNC_PROGRESS', {
progress: this.playerApi?.getCurrentTime(),
state: this.playerApi?.getPlayerState(),
index
index,
});
}, 1000);
@ -593,18 +673,25 @@ export default createPlugin<
onItemClick: (id) => {
if (id === 'music-together-close') {
this.onStop();
this.api?.toastService?.show(t('plugins.music-together.toast.closed'));
this.api?.toastService?.show(
t('plugins.music-together.toast.closed'),
);
hostPopup.dismiss();
}
if (id === 'music-together-copy-id') {
navigator.clipboard.writeText(this.connection?.id ?? '')
navigator.clipboard
.writeText(this.connection?.id ?? '')
.then(() => {
this.api?.toastService?.show(t('plugins.music-together.toast.id-copied'));
this.api?.toastService?.show(
t('plugins.music-together.toast.id-copied'),
);
hostPopup.dismiss();
})
.catch(() => {
this.api?.toastService?.show(t('plugins.music-together.toast.id-copy-failed'));
this.api?.toastService?.show(
t('plugins.music-together.toast.id-copy-failed'),
);
hostPopup.dismiss();
});
}
@ -612,30 +699,39 @@ export default createPlugin<
if (id === 'music-together-permission') {
if (this.permission === 'all') this.permission = 'host-only';
else if (this.permission === 'playlist') this.permission = 'all';
else if (this.permission === 'host-only') this.permission = 'playlist';
else if (this.permission === 'host-only')
this.permission = 'playlist';
this.connection?.broadcast('PERMISSION', this.permission);
hostPopup.setPermission(this.permission);
guestPopup.setPermission(this.permission);
settingPopup.setPermission(this.permission);
const permissionLabel = t(`plugins.music-together.menu.permission.${this.permission}`);
this.api?.toastService?.show(t('plugins.music-together.toast.permission-changed', { permission: permissionLabel }));
const permissionLabel = t(
`plugins.music-together.menu.permission.${this.permission}`,
);
this.api?.toastService?.show(
t('plugins.music-together.toast.permission-changed', {
permission: permissionLabel,
}),
);
const item = hostPopup.items.find((it) => it?.element.id === id);
if (item?.type === 'item') {
item.setText(t('plugins.music-together.menu.set-permission'));
}
}
}
},
});
const guestPopup = createGuestPopup({
onItemClick: (id) => {
if (id === 'music-together-disconnect') {
this.onStop();
this.api?.toastService?.show(t('plugins.music-together.toast.disconnected'));
this.api?.toastService?.show(
t('plugins.music-together.toast.disconnected'),
);
guestPopup.dismiss();
}
}
},
});
const settingPopup = createSettingPopup({
onItemClick: async (id) => {
@ -646,16 +742,24 @@ export default createPlugin<
this.hideSpinner();
if (result) {
navigator.clipboard.writeText(this.connection?.id ?? '')
navigator.clipboard
.writeText(this.connection?.id ?? '')
.then(() => {
this.api?.toastService?.show(t('plugins.music-together.toast.id-copied'));
this.api?.toastService?.show(
t('plugins.music-together.toast.id-copied'),
);
hostPopup.showAtAnchor(setting);
}).catch(() => {
this.api?.toastService?.show(t('plugins.music-together.toast.id-copy-failed'));
})
.catch(() => {
this.api?.toastService?.show(
t('plugins.music-together.toast.id-copy-failed'),
);
hostPopup.showAtAnchor(setting);
});
} else {
this.api?.toastService?.show(t('plugins.music-together.toast.host-failed'));
this.api?.toastService?.show(
t('plugins.music-together.toast.host-failed'),
);
}
}
@ -666,18 +770,22 @@ export default createPlugin<
this.hideSpinner();
if (result) {
this.api?.toastService?.show(t('plugins.music-together.toast.joined'));
this.api?.toastService?.show(
t('plugins.music-together.toast.joined'),
);
guestPopup.showAtAnchor(setting);
} else {
this.api?.toastService?.show(t('plugins.music-together.toast.join-failed'));
this.api?.toastService?.show(
t('plugins.music-together.toast.join-failed'),
);
}
}
}
},
});
this.popups = {
host: hostPopup,
guest: guestPopup,
setting: settingPopup
setting: settingPopup,
};
setting.addEventListener('click', () => {
let popup = settingPopup;
@ -695,24 +803,38 @@ export default createPlugin<
this.queue = new Queue({
owner: {
id: this.connection?.id ?? '',
...this.me!
...this.me!,
},
getProfile: (id) => this.profiles[id]
getProfile: (id) => this.profiles[id],
});
this.playerApi = playerApi;
this.playerApi.addEventListener('onStateChange', this.videoStateChangeListener);
this.playerApi.addEventListener(
'onStateChange',
this.videoStateChangeListener,
);
document.addEventListener('videodatachange', this.videoChangeListener);
},
stop() {
const dividers = Array.from(document.querySelectorAll('.music-together-divider'));
const dividers = Array.from(
document.querySelectorAll('.music-together-divider'),
);
dividers.forEach((divider) => divider.remove());
this.elements.setting?.remove();
this.onStop();
if (typeof this.stateInterval === 'number') clearInterval(this.stateInterval);
if (this.playerApi) this.playerApi.removeEventListener('onStateChange', this.videoStateChangeListener);
if (this.videoChangeListener) document.removeEventListener('videodatachange', this.videoChangeListener);
}
}
if (typeof this.stateInterval === 'number')
clearInterval(this.stateInterval);
if (this.playerApi)
this.playerApi.removeEventListener(
'onStateChange',
this.videoStateChangeListener,
);
if (this.videoChangeListener)
document.removeEventListener(
'videodatachange',
this.videoChangeListener,
);
},
},
});

View File

@ -1,11 +1,20 @@
import { SHA1Hash } from './sha1hash';
export const extractToken = (cookie = document.cookie) => cookie.match(/SAPISID=([^;]+);/)?.[1] ?? cookie.match(/__Secure-3PAPISID=([^;]+);/)?.[1];
export const extractToken = (cookie = document.cookie) =>
cookie.match(/SAPISID=([^;]+);/)?.[1] ??
cookie.match(/__Secure-3PAPISID=([^;]+);/)?.[1];
export const getHash = async (papisid: string, millis = Date.now(), origin: string = 'https://music.youtube.com') =>
(await SHA1Hash(`${millis} ${papisid} ${origin}`)).toLowerCase();
export const getHash = async (
papisid: string,
millis = Date.now(),
origin: string = 'https://music.youtube.com',
) => (await SHA1Hash(`${millis} ${papisid} ${origin}`)).toLowerCase();
export const getAuthorizationHeader = async (papisid: string, millis = Date.now(), origin: string = 'https://music.youtube.com') => {
export const getAuthorizationHeader = async (
papisid: string,
millis = Date.now(),
origin: string = 'https://music.youtube.com',
) => {
return `SAPISIDHASH ${millis}_${await getHash(papisid, millis, origin)}`;
};
@ -23,15 +32,17 @@ export const getClient = () => {
platform: 'DESKTOP',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
locationInfo: {
locationPermissionAuthorizationStatus: 'LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED',
locationPermissionAuthorizationStatus:
'LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED',
},
musicAppInfo: {
pwaInstallabilityStatus: 'PWA_INSTALLABILITY_STATUS_UNKNOWN',
webDisplayMode: 'WEB_DISPLAY_MODE_BROWSER',
storeDigitalGoodsApiSupportStatus: {
playStoreDigitalGoodsApiSupportStatus: 'DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED',
playStoreDigitalGoodsApiSupportStatus:
'DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED',
},
},
utcOffsetMinutes: -1 * (new Date()).getTimezoneOffset(),
utcOffsetMinutes: -1 * new Date().getTimezoneOffset(),
};
};

View File

@ -54,46 +54,46 @@ const getHeaderPayload = (() => {
title: {
runs: [
{
text: t('plugins.music-together.internal.track-source')
}
]
text: t('plugins.music-together.internal.track-source'),
},
],
},
subtitle: {
runs: [
{
text: t('plugins.music-together.name')
}
]
text: t('plugins.music-together.name'),
},
],
},
buttons: [
{
chipCloudChipRenderer: {
style: {
styleType: 'STYLE_TRANSPARENT'
styleType: 'STYLE_TRANSPARENT',
},
text: {
runs: [
{
text: t('plugins.music-together.internal.save')
}
]
text: t('plugins.music-together.internal.save'),
},
],
},
navigationEndpoint: {
saveQueueToPlaylistCommand: {}
saveQueueToPlaylistCommand: {},
},
icon: {
iconType: 'ADD_TO_PLAYLIST'
iconType: 'ADD_TO_PLAYLIST',
},
accessibilityData: {
accessibilityData: {
label: t('plugins.music-together.internal.save')
}
label: t('plugins.music-together.internal.save'),
},
},
isSelected: false,
uniqueId: t('plugins.music-together.internal.save')
}
}
]
uniqueId: t('plugins.music-together.internal.save'),
},
},
],
};
}
@ -106,7 +106,7 @@ export type QueueOptions = {
owner?: Profile;
queue?: QueueElement;
getProfile: (id: string) => Profile | undefined;
}
};
export type QueueEventListener = (event: ConnectionEventUnion) => void;
export class Queue {
@ -114,7 +114,7 @@ export class Queue {
private originalDispatch?: (obj: {
type: string;
payload?: { items?: QueueItem[] | undefined; };
payload?: { items?: QueueItem[] | undefined };
}) => void;
private internalDispatch = false;
@ -126,7 +126,8 @@ export class Queue {
constructor(options: QueueOptions) {
this.getProfile = options.getProfile;
this.queue = options.queue ?? (document.querySelector<QueueElement>('#queue')!);
this.queue =
options.queue ?? document.querySelector<QueueElement>('#queue')!;
this.owner = options.owner ?? null;
this._videoList = options.videoList ?? [];
}
@ -139,7 +140,12 @@ export class Queue {
}
get selectedIndex() {
return mapQueueItem((it) => it?.selected, this.queue.queue.store.store.getState().queue.items).findIndex(Boolean) ?? 0;
return (
mapQueueItem(
(it) => it?.selected,
this.queue.queue.store.store.getState().queue.items,
).findIndex(Boolean) ?? 0
);
}
get rawItems() {
@ -162,7 +168,9 @@ export class Queue {
}
async addVideos(videos: VideoData[], index?: number) {
const response = await getMusicQueueRenderer(videos.map((it) => it.videoId));
const response = await getMusicQueueRenderer(
videos.map((it) => it.videoId),
);
if (!response) return false;
const items = response.queueDatas.map((it) => it?.content).filter(Boolean);
@ -173,12 +181,16 @@ export class Queue {
this.queue?.dispatch({
type: 'ADD_ITEMS',
payload: {
nextQueueItemId: this.queue.queue.store.store.getState().queue.nextQueueItemId,
index: index ?? this.queue.queue.store.store.getState().queue.items.length ?? 0,
nextQueueItemId:
this.queue.queue.store.store.getState().queue.nextQueueItemId,
index:
index ??
this.queue.queue.store.store.getState().queue.items.length ??
0,
items,
shuffleEnabled: false,
shouldAssignIds: true
}
shouldAssignIds: true,
},
});
this.internalDispatch = false;
setTimeout(() => {
@ -194,7 +206,7 @@ export class Queue {
this._videoList.splice(index, 1);
this.queue?.dispatch({
type: 'REMOVE_ITEM',
payload: index
payload: index,
});
this.internalDispatch = false;
setTimeout(() => {
@ -207,7 +219,7 @@ export class Queue {
this.internalDispatch = true;
this.queue?.dispatch({
type: 'SET_INDEX',
payload: index
payload: index,
});
this.internalDispatch = false;
}
@ -220,8 +232,8 @@ export class Queue {
type: 'MOVE_ITEM',
payload: {
fromIndex,
toIndex
}
toIndex,
},
});
this.internalDispatch = false;
setTimeout(() => {
@ -234,7 +246,7 @@ export class Queue {
this.internalDispatch = true;
this._videoList = [];
this.queue?.dispatch({
type: 'CLEAR'
type: 'CLEAR',
});
this.internalDispatch = false;
}
@ -253,7 +265,8 @@ export class Queue {
return;
}
if (this.originalDispatch) this.queue.queue.store.store.dispatch = this.originalDispatch;
if (this.originalDispatch)
this.queue.queue.store.store.dispatch = this.originalDispatch;
}
injection() {
@ -276,40 +289,54 @@ export class Queue {
if (event.type === 'ADD_ITEMS') {
if (this.ignoreFlag) {
this.ignoreFlag = false;
const videoList = mapQueueItem((it) => ({
videoId: it!.videoId,
ownerId: this.owner!.id
} satisfies VideoData), event.payload!.items!);
const videoList = mapQueueItem(
(it) =>
({
videoId: it!.videoId,
ownerId: this.owner!.id,
}) satisfies VideoData,
event.payload!.items!,
);
const index = this._videoList.length + videoList.length - 1;
if (videoList.length > 0) {
this.broadcast({ // play
this.broadcast({
// play
type: 'ADD_SONGS',
payload: {
videoList
videoList,
},
after: [
{
type: 'SYNC_PROGRESS',
payload: {
index
}
}
]
index,
},
},
],
});
}
} else if ((event.payload as {
items: unknown[];
}).items.length === 1) {
this.broadcast({ // add playlist
} else if (
(
event.payload as {
items: unknown[];
}
).items.length === 1
) {
this.broadcast({
// add playlist
type: 'ADD_SONGS',
payload: {
// index: (event.payload as any).index,
videoList: mapQueueItem((it) => ({
videoId: it!.videoId,
ownerId: this.owner!.id
} satisfies VideoData), event.payload!.items!)
}
videoList: mapQueueItem(
(it) =>
({
videoId: it!.videoId,
ownerId: this.owner!.id,
}) satisfies VideoData,
event.payload!.items!,
),
},
});
}
@ -320,13 +347,17 @@ export class Queue {
this.broadcast({
type: 'MOVE_SONG',
payload: {
fromIndex: (event.payload as {
fromIndex: number;
}).fromIndex,
toIndex: (event.payload as {
toIndex: number;
}).toIndex
}
fromIndex: (
event.payload as {
fromIndex: number;
}
).fromIndex,
toIndex: (
event.payload as {
toIndex: number;
}
).toIndex,
},
});
return;
}
@ -334,8 +365,8 @@ export class Queue {
this.broadcast({
type: 'REMOVE_SONG',
payload: {
index: event.payload as number
}
index: event.payload as number,
},
});
return;
}
@ -343,8 +374,8 @@ export class Queue {
this.broadcast({
type: 'SYNC_PROGRESS',
payload: {
index: event.payload as number
}
index: event.payload as number,
},
});
return;
}
@ -355,7 +386,10 @@ export class Queue {
event.payload = undefined;
}
if (event.type === 'SET_PLAYER_UI_STATE') {
if (event.payload as string === 'INACTIVE' && this.videoList.length > 0) {
if (
(event.payload as string) === 'INACTIVE' &&
this.videoList.length > 0
) {
return;
}
}
@ -370,7 +404,7 @@ export class Queue {
store: {
...this.queue.queue.store,
dispatch: this.originalDispatch,
}
},
},
};
this.originalDispatch?.call(fakeContext, event);
@ -384,20 +418,22 @@ export class Queue {
this.internalDispatch = true;
this.queue.dispatch({
type: 'HAS_SHOWN_AUTOPLAY',
payload: false
payload: false,
});
this.queue.dispatch({
type: 'SET_HEADER',
payload: getHeaderPayload(),
});
this.queue.dispatch({
type: 'CLEAR_STEERING_CHIPS'
type: 'CLEAR_STEERING_CHIPS',
});
this.internalDispatch = false;
}
async syncVideo() {
const response = await getMusicQueueRenderer(this._videoList.map((it) => it.videoId));
const response = await getMusicQueueRenderer(
this._videoList.map((it) => it.videoId),
);
if (!response) return false;
const items = response.queueDatas.map((it) => it.content);
@ -407,10 +443,11 @@ export class Queue {
type: 'UPDATE_ITEMS',
payload: {
items: items,
nextQueueItemId: this.queue.queue.store.store.getState().queue.nextQueueItemId,
nextQueueItemId:
this.queue.queue.store.store.getState().queue.nextQueueItemId,
shouldAssignIds: true,
currentIndex: -1
}
currentIndex: -1,
},
});
this.internalDispatch = false;
setTimeout(() => {
@ -425,7 +462,9 @@ export class Queue {
const allQueue = document.querySelectorAll('#queue');
allQueue.forEach((queue) => {
const list = Array.from(queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? []);
const list = Array.from(
queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? [],
);
list.forEach((item, index: number | undefined) => {
if (typeof index !== 'number') return;
@ -433,14 +472,19 @@ export class Queue {
const id = this._videoList[index]?.ownerId;
const data = this.getProfile(id);
const profile = item.querySelector<HTMLImageElement>('.music-together-owner') ?? document.createElement('img');
const profile =
item.querySelector<HTMLImageElement>('.music-together-owner') ??
document.createElement('img');
profile.classList.add('music-together-owner');
profile.dataset.id = id;
profile.dataset.index = index.toString();
const name = item.querySelector<HTMLElement>('.music-together-name') ?? document.createElement('div');
const name =
item.querySelector<HTMLElement>('.music-together-name') ??
document.createElement('div');
name.classList.add('music-together-name');
name.textContent = data?.name ?? t('plugins.music-together.internal.unknown-user');
name.textContent =
data?.name ?? t('plugins.music-together.internal.unknown-user');
if (data) {
profile.dataset.thumbnail = data.thumbnail ?? '';
@ -463,10 +507,14 @@ export class Queue {
const allQueue = document.querySelectorAll('#queue');
allQueue.forEach((queue) => {
const list = Array.from(queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? []);
const list = Array.from(
queue?.querySelectorAll<HTMLElement>('ytmusic-player-queue-item') ?? [],
);
list.forEach((item) => {
const profile = item.querySelector<HTMLImageElement>('.music-together-owner');
const profile = item.querySelector<HTMLImageElement>(
'.music-together-owner',
);
const name = item.querySelector<HTMLElement>('.music-together-name');
profile?.remove();
name?.remove();

View File

@ -8,7 +8,9 @@ type QueueRendererResponse = {
trackingParams: string;
};
export const getMusicQueueRenderer = async (videoIds: string[]): Promise<QueueRendererResponse | null> => {
export const getMusicQueueRenderer = async (
videoIds: string[],
): Promise<QueueRendererResponse | null> => {
const token = extractToken();
if (!token) return null;
@ -35,8 +37,8 @@ export const getMusicQueueRenderer = async (videoIds: string[]): Promise<QueueRe
'Content-Type': 'application/json',
'Origin': 'https://music.youtube.com',
'Authorization': await getAuthorizationHeader(token),
}
}
},
},
);
const text = await response.text();

View File

@ -1,21 +1,26 @@
import {
ItemPlaylistPanelVideoRenderer,
PlaylistPanelVideoWrapperRenderer,
QueueItem
QueueItem,
} from '@/types/datahost-get-state';
export const mapQueueItem = <T>(map: (item?: ItemPlaylistPanelVideoRenderer) => T, array: QueueItem[]): T[] => array
.map((item) => {
if ('playlistPanelVideoWrapperRenderer' in item) {
const keys = Object.keys(item.playlistPanelVideoWrapperRenderer!.primaryRenderer) as (keyof PlaylistPanelVideoWrapperRenderer['primaryRenderer'])[];
return item.playlistPanelVideoWrapperRenderer!.primaryRenderer[keys[0]];
}
if ('playlistPanelVideoRenderer' in item) {
return item.playlistPanelVideoRenderer;
}
console.error('Music Together: Unknown item', item);
return undefined;
})
.map(map);
export const mapQueueItem = <T>(
map: (item?: ItemPlaylistPanelVideoRenderer) => T,
array: QueueItem[],
): T[] =>
array
.map((item) => {
if ('playlistPanelVideoWrapperRenderer' in item) {
const keys = Object.keys(
item.playlistPanelVideoWrapperRenderer!.primaryRenderer,
) as (keyof PlaylistPanelVideoWrapperRenderer['primaryRenderer'])[];
return item.playlistPanelVideoWrapperRenderer!.primaryRenderer[keys[0]];
}
if ('playlistPanelVideoRenderer' in item) {
return item.playlistPanelVideoRenderer;
}
console.error('Music Together: Unknown item', item);
return undefined;
})
.map(map);

View File

@ -10,13 +10,16 @@ export type VideoData = {
};
export type Permission = 'host-only' | 'playlist' | 'all';
export const getDefaultProfile = (connectionID: string, id: string = Date.now().toString()): Profile => {
export const getDefaultProfile = (
connectionID: string,
id: string = Date.now().toString(),
): Profile => {
const name = `Guest ${id.slice(0, 4)}`;
return {
id: connectionID,
handleId: `#music-together:${id}`,
name,
thumbnail: `https://ui-avatars.com/api/?name=${name}&background=random`
thumbnail: `https://ui-avatars.com/api/?name=${name}&background=random`,
};
};

View File

@ -7,7 +7,6 @@ import { createStatus } from '../ui/status';
import IconOff from '../icons/off.svg?raw';
export type GuestPopupProps = {
onItemClick: (id: string) => void;
};
@ -33,7 +32,7 @@ export const createGuestPopup = (props: GuestPopupProps) => {
},
],
anchorAt: 'bottom-right',
popupAt: 'top-right'
popupAt: 'top-right',
});
return {

View File

@ -22,7 +22,7 @@ export const createHostPopup = (props: HostPopupProps) => {
element: status.element,
},
{
type: 'divider'
type: 'divider',
},
{
id: 'music-together-copy-id',
@ -35,7 +35,9 @@ export const createHostPopup = (props: HostPopupProps) => {
id: 'music-together-permission',
type: 'item',
icon: ElementFromHtml(IconTune),
text: t('plugins.music-together.menu.set-permission', { permission: t('plugins.music-together.menu.permission.host-only') }),
text: t('plugins.music-together.menu.set-permission', {
permission: t('plugins.music-together.menu.permission.host-only'),
}),
onClick: () => props.onItemClick('music-together-permission'),
},
{

View File

@ -39,7 +39,7 @@ export const createSettingPopup = (props: SettingPopupProps) => {
},
],
anchorAt: 'bottom-right',
popupAt: 'top-right'
popupAt: 'top-right',
});
return {

View File

@ -7,17 +7,27 @@ import type { Permission, Profile } from '../types';
export const createStatus = () => {
const element = ElementFromHtml(statusHTML);
const icon = document.querySelector<HTMLImageElement>('ytmusic-settings-button > tp-yt-paper-icon-button > tp-yt-iron-icon#icon img');
const icon = document.querySelector<HTMLImageElement>(
'ytmusic-settings-button > tp-yt-paper-icon-button > tp-yt-iron-icon#icon img',
);
const profile = element.querySelector<HTMLImageElement>('.music-together-profile')!;
const statusLabel = element.querySelector<HTMLSpanElement>('#music-together-status-label')!;
const permissionLabel = element.querySelector<HTMLMarqueeElement>('#music-together-permission-label')!;
const profile = element.querySelector<HTMLImageElement>(
'.music-together-profile',
)!;
const statusLabel = element.querySelector<HTMLSpanElement>(
'#music-together-status-label',
)!;
const permissionLabel = element.querySelector<HTMLMarqueeElement>(
'#music-together-permission-label',
)!;
profile.src = icon?.src ?? '';
const setStatus = (status: 'disconnected' | 'host' | 'guest') => {
if (status === 'disconnected') {
statusLabel.textContent = t('plugins.music-together.menu.status.disconnected');
statusLabel.textContent = t(
'plugins.music-together.menu.status.disconnected',
);
statusLabel.style.color = 'rgba(255, 255, 255, 0.5)';
}
@ -34,17 +44,23 @@ export const createStatus = () => {
const setPermission = (permission: Permission) => {
if (permission === 'host-only') {
permissionLabel.textContent = t('plugins.music-together.menu.permission.host-only');
permissionLabel.textContent = t(
'plugins.music-together.menu.permission.host-only',
);
permissionLabel.style.color = 'rgba(255, 255, 255, 0.5)';
}
if (permission === 'playlist') {
permissionLabel.textContent = t('plugins.music-together.menu.permission.playlist');
permissionLabel.textContent = t(
'plugins.music-together.menu.permission.playlist',
);
permissionLabel.style.color = 'rgba(255, 255, 255, 0.75)';
}
if (permission === 'all') {
permissionLabel.textContent = t('plugins.music-together.menu.permission.all');
permissionLabel.textContent = t(
'plugins.music-together.menu.permission.all',
);
permissionLabel.style.color = 'rgba(255, 255, 255, 1)';
}
};
@ -54,7 +70,9 @@ export const createStatus = () => {
};
const setUsers = (users: Profile[]) => {
const container = element.querySelector<HTMLDivElement>('.music-together-user-container')!;
const container = element.querySelector<HTMLDivElement>(
'.music-together-user-container',
)!;
const empty = element.querySelector<HTMLElement>('.music-together-empty')!;
for (const child of Array.from(container.children)) {
if (child !== empty) child.remove();