feat: updates

This commit is contained in:
qier222 2022-06-12 15:29:14 +08:00
parent 196a974a64
commit 8f4c3d8e5b
No known key found for this signature in database
GPG key ID: 9C85007ED905F14D
24 changed files with 572 additions and 93 deletions

View file

@ -0,0 +1,84 @@
import { logger } from '@sentry/utils'
import axios from 'axios'
// 'https://mvod.itunes.apple.com/itunes-assets/HLSMusic116/v4/de/52/95/de52957b-fcf1-ae96-b114-0445cb8c41d4/P359420813_default.m3u8'
const searchAlbum = async (
keyword: string
): Promise<
| {
id: string
href: string
attributes: {
artistName: string
url: string
name: string
editorialNotes?: {
standard: string
short: string
}
}
}
| undefined
> => {
const r = await axios.get(
`https://amp-api.music.apple.com/v1/catalog/cn/search`,
{
params: {
term: keyword,
l: 'zh-cn',
platform: 'web',
types: 'albums',
limit: 1,
},
headers: {
authorization: 'Bearer xxxxxx', // required token
},
}
)
return r.data?.results?.albums?.data?.[0]
}
export const getCoverVideo = async ({
name,
artists,
}: {
name: string
artists: string[]
}): Promise<string | undefined> => {
const keyword = `${artists.join(' ')} ${name}`
logger.debug(`[appleMusic] getCoverVideo: ${keyword}`)
const album = await searchAlbum(keyword).catch(e => {
console.log(e)
logger.debug('[appleMusic] Search album error', e)
})
const url = album?.attributes.url
if (!url) {
logger.info('[appleMusic] no url')
return
}
let { data: html } = await axios.get(url)
if (!html) return
const regex =
/<script type="fastboot\/shoebox" id="shoebox-media-api-cache-amp-music">(.*?)<\/script>/
html = html
.match(regex)[0]
.replace(
'<script type="fastboot/shoebox" id="shoebox-media-api-cache-amp-music">',
''
)
.replace('</script>', '')
html = JSON.parse(html)
const data = JSON.parse(html[Object.keys(html)[1]])
const m3u8 =
data?.d?.[0]?.attributes?.editorialVideo?.motionSquareVideo1x1?.video
logger.debug(`[appleMusic] ${m3u8}`)
return m3u8
}

View file

@ -31,8 +31,9 @@ class Cache {
break break
} }
case APIs.Track: { case APIs.Track: {
if (!data.songs) return const res = data as FetchTracksResponse
const tracks = (data as FetchTracksResponse).songs.map(t => ({ if (!res.songs) return
const tracks = res.songs.map(t => ({
id: t.id, id: t.id,
json: JSON.stringify(t), json: JSON.stringify(t),
updatedAt: Date.now(), updatedAt: Date.now(),
@ -106,6 +107,16 @@ class Cache {
db.upsert(Tables.CoverColor, { db.upsert(Tables.CoverColor, {
id: data.id, id: data.id,
color: data.color, color: data.color,
queriedAt: Date.now(),
})
break
}
case APIs.VideoCover: {
if (!data.id) return
db.upsert(Tables.VideoCover, {
id: data.id,
url: data.url || 'no',
queriedAt: Date.now(),
}) })
} }
} }
@ -196,6 +207,10 @@ class Cache {
if (isNaN(Number(params?.id))) return if (isNaN(Number(params?.id))) return
return db.find(Tables.CoverColor, params.id)?.color return db.find(Tables.CoverColor, params.id)?.color
} }
case APIs.VideoCover: {
if (isNaN(Number(params?.id))) return
return db.find(Tables.VideoCover, params.id)?.url
}
} }
} }
@ -278,7 +293,7 @@ class Cache {
br, br,
type: type as TablesStructures[Tables.Audio]['type'], type: type as TablesStructures[Tables.Audio]['type'],
source, source,
updatedAt: Date.now(), queriedAt: Date.now(),
}) })
log.info(`[cache] cacheAudio ${id}-${br}.${type}`) log.info(`[cache] cacheAudio ${id}-${br}.${type}`)

View file

@ -18,6 +18,7 @@ export const enum Tables {
AccountData = 'AccountData', AccountData = 'AccountData',
CoverColor = 'CoverColor', CoverColor = 'CoverColor',
AppData = 'AppData', AppData = 'AppData',
VideoCover = 'VideoCover',
} }
interface CommonTableStructure { interface CommonTableStructure {
id: number id: number
@ -50,16 +51,22 @@ export interface TablesStructures {
| 'qq' | 'qq'
| 'bilibili' | 'bilibili'
| 'joox' | 'joox'
updatedAt: number queriedAt: number
} }
[Tables.CoverColor]: { [Tables.CoverColor]: {
id: number id: number
color: string color: string
queriedAt: number
} }
[Tables.AppData]: { [Tables.AppData]: {
id: 'appVersion' | 'skippedVersion' id: 'appVersion' | 'skippedVersion'
value: string value: string
} }
[Tables.VideoCover]: {
id: number
url: string
queriedAt: number
}
} }
type TableNames = keyof TablesStructures type TableNames = keyof TablesStructures

View file

@ -12,6 +12,7 @@ import { Thumbar } from './windowsTaskbar'
import fastFolderSize from 'fast-folder-size' import fastFolderSize from 'fast-folder-size'
import path from 'path' import path from 'path'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import { getCoverVideo } from './appleMusic'
const on = <T extends keyof IpcChannelsParams>( const on = <T extends keyof IpcChannelsParams>(
channel: T, channel: T,
@ -20,6 +21,16 @@ const on = <T extends keyof IpcChannelsParams>(
ipcMain.on(channel, listener) ipcMain.on(channel, listener)
} }
const handle = <T extends keyof IpcChannelsParams>(
channel: T,
listener: (
event: Electron.IpcMainInvokeEvent,
params: IpcChannelsParams[T]
) => void
) => {
return ipcMain.handle(channel, listener)
}
export function initIpcMain( export function initIpcMain(
win: BrowserWindow | null, win: BrowserWindow | null,
tray: YPMTray | null, tray: YPMTray | null,
@ -143,6 +154,22 @@ function initOtherIpcMain() {
) )
}) })
/**
*
*/
on(IpcChannels.SetVideoCover, (event, args) => {
const { id, url } = args
cache.set(APIs.VideoCover, { id, url })
})
/**
*
*/
on(IpcChannels.GetVideoCover, (event, args) => {
const { id } = args
event.returnValue = cache.get(APIs.VideoCover, { id })
})
/** /**
* tables到json文件便table大小dev环境 * tables到json文件便table大小dev环境
*/ */

View file

@ -11,6 +11,7 @@ contextBridge.exposeInMainWorld('log', log)
contextBridge.exposeInMainWorld('ipcRenderer', { contextBridge.exposeInMainWorld('ipcRenderer', {
sendSync: ipcRenderer.sendSync, sendSync: ipcRenderer.sendSync,
invoke: ipcRenderer.invoke,
send: ipcRenderer.send, send: ipcRenderer.send,
on: ( on: (
channel: IpcChannels, channel: IpcChannels,

View file

@ -2,9 +2,11 @@ CREATE TABLE IF NOT EXISTS "AccountData" ("id" text NOT NULL,"json" text NOT NUL
CREATE TABLE IF NOT EXISTS "Album" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "Album" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS "ArtistAlbum" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "ArtistAlbum" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS "Artist" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "Artist" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS "Audio" ("id" integer NOT NULL,"br" int NOT NULL,"type" text NOT NULL,"source" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS "Lyric" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" integer NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "Lyric" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" integer NOT NULL, PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS "Playlist" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "Playlist" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS "Track" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "Track" ("id" integer NOT NULL,"json" text NOT NULL,"updatedAt" int NOT NULL, PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS "CoverColor" ("id" integer NOT NULL,"color" text NOT NULL, PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS "AppData" ("id" text NOT NULL,"value" text, PRIMARY KEY (id)); CREATE TABLE IF NOT EXISTS "AppData" ("id" text NOT NULL,"value" text, PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS "CoverColor" ("id" integer NOT NULL,"color" text NOT NULL, "queriedAt" int NOT NULL, PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS "Audio" ("id" integer NOT NULL,"br" int NOT NULL,"type" text NOT NULL,"source" text NOT NULL,"updatedAt" int NOT NULL, "queriedAt" int NOT NULL, PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS "VideoCover" ("id" integer NOT NULL,"url" text NOT NULL,"updatedAt" int NOT NULL, "queriedAt" int NOT NULL, PRIMARY KEY (id));

View file

@ -9,7 +9,7 @@
"dev": "node scripts/build.main.mjs --watch", "dev": "node scripts/build.main.mjs --watch",
"build": "node scripts/build.main.mjs", "build": "node scripts/build.main.mjs",
"pack": "electron-builder build -c .electron-builder.config.js", "pack": "electron-builder build -c .electron-builder.config.js",
"test:types": "tsc --noEmit --project src/main/tsconfig.json", "test:types": "tsc --noEmit --project ./tsconfig.json",
"lint": "eslint --ext .ts,.js ./", "lint": "eslint --ext .ts,.js ./",
"format": "prettier --write './**/*.{ts,js,tsx,jsx}'" "format": "prettier --write './**/*.{ts,js,tsx,jsx}'"
}, },
@ -29,6 +29,7 @@
"electron-store": "^8.0.1", "electron-store": "^8.0.1",
"express": "^4.18.1", "express": "^4.18.1",
"fast-folder-size": "^1.7.0", "fast-folder-size": "^1.7.0",
"m3u8-parser": "^4.7.1",
"pretty-bytes": "^6.0.0" "pretty-bytes": "^6.0.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -21,7 +21,6 @@ export const enum APIs {
Album = 'album', Album = 'album',
Artist = 'artists', Artist = 'artists',
ArtistAlbum = 'artist/album', ArtistAlbum = 'artist/album',
CoverColor = 'cover_color',
Likelist = 'likelist', Likelist = 'likelist',
Lyric = 'lyric', Lyric = 'lyric',
Personalized = 'personalized', Personalized = 'personalized',
@ -33,13 +32,16 @@ export const enum APIs {
UserAlbums = 'album/sublist', UserAlbums = 'album/sublist',
UserArtists = 'artist/sublist', UserArtists = 'artist/sublist',
UserPlaylist = 'user/playlist', UserPlaylist = 'user/playlist',
// not netease api
CoverColor = 'cover_color',
VideoCover = 'video_cover',
} }
export interface APIsParams { export interface APIsParams {
[APIs.Album]: { id: number } [APIs.Album]: { id: number }
[APIs.Artist]: { id: number } [APIs.Artist]: { id: number }
[APIs.ArtistAlbum]: { id: number } [APIs.ArtistAlbum]: { id: number }
[APIs.CoverColor]: { id: number }
[APIs.Likelist]: void [APIs.Likelist]: void
[APIs.Lyric]: { id: number } [APIs.Lyric]: { id: number }
[APIs.Personalized]: void [APIs.Personalized]: void
@ -51,13 +53,14 @@ export interface APIsParams {
[APIs.UserAlbums]: void [APIs.UserAlbums]: void
[APIs.UserArtists]: void [APIs.UserArtists]: void
[APIs.UserPlaylist]: void [APIs.UserPlaylist]: void
[APIs.CoverColor]: { id: number }
[APIs.VideoCover]: { id: number }
} }
export interface APIsResponse { export interface APIsResponse {
[APIs.Album]: FetchAlbumResponse [APIs.Album]: FetchAlbumResponse
[APIs.Artist]: FetchArtistResponse [APIs.Artist]: FetchArtistResponse
[APIs.ArtistAlbum]: FetchArtistAlbumsResponse [APIs.ArtistAlbum]: FetchArtistAlbumsResponse
[APIs.CoverColor]: string | undefined
[APIs.Likelist]: FetchUserLikedTracksIDsResponse [APIs.Likelist]: FetchUserLikedTracksIDsResponse
[APIs.Lyric]: FetchLyricResponse [APIs.Lyric]: FetchLyricResponse
[APIs.Personalized]: FetchRecommendedPlaylistsResponse [APIs.Personalized]: FetchRecommendedPlaylistsResponse
@ -69,4 +72,6 @@ export interface APIsResponse {
[APIs.UserAlbums]: FetchUserAlbumsResponse [APIs.UserAlbums]: FetchUserAlbumsResponse
[APIs.UserArtists]: FetchUserArtistsResponse [APIs.UserArtists]: FetchUserArtistsResponse
[APIs.UserPlaylist]: FetchUserPlaylistsResponse [APIs.UserPlaylist]: FetchUserPlaylistsResponse
[APIs.CoverColor]: string | undefined
[APIs.VideoCover]: string | undefined
} }

View file

@ -23,6 +23,8 @@ export const enum IpcChannels {
SyncSettings = 'SyncSettings', SyncSettings = 'SyncSettings',
GetAudioCacheSize = 'GetAudioCacheSize', GetAudioCacheSize = 'GetAudioCacheSize',
ResetWindowSize = 'ResetWindowSize', ResetWindowSize = 'ResetWindowSize',
GetVideoCover = 'GetVideoCover',
SetVideoCover = 'SetVideoCover',
} }
// ipcMain.on params // ipcMain.on params
@ -58,6 +60,8 @@ export interface IpcChannelsParams {
[IpcChannels.SyncSettings]: Store['settings'] [IpcChannels.SyncSettings]: Store['settings']
[IpcChannels.GetAudioCacheSize]: void [IpcChannels.GetAudioCacheSize]: void
[IpcChannels.ResetWindowSize]: void [IpcChannels.ResetWindowSize]: void
[IpcChannels.GetVideoCover]: { id: number }
[IpcChannels.SetVideoCover]: { id: number; url: string }
} }
// ipcRenderer.on params // ipcRenderer.on params
@ -79,4 +83,6 @@ export interface IpcChannelsReturns {
[IpcChannels.Like]: void [IpcChannels.Like]: void
[IpcChannels.Repeat]: RepeatMode [IpcChannels.Repeat]: RepeatMode
[IpcChannels.GetAudioCacheSize]: void [IpcChannels.GetAudioCacheSize]: void
[IpcChannels.GetVideoCover]: string | undefined
[IpcChannels.SetVideoCover]: void
} }

View file

@ -22,7 +22,7 @@ const Image = ({
className?: string className?: string
alt: string alt: string
lazyLoad?: boolean lazyLoad?: boolean
placeholder?: 'artist' | 'album' | 'playlist' | 'podcast' | 'blank' | null placeholder?: 'artist' | 'album' | 'playlist' | 'podcast' | 'blank' | false
onClick?: (e: React.MouseEvent<HTMLImageElement>) => void onClick?: (e: React.MouseEvent<HTMLImageElement>) => void
onMouseOver?: (e: React.MouseEvent<HTMLImageElement>) => void onMouseOver?: (e: React.MouseEvent<HTMLImageElement>) => void
animation?: boolean animation?: boolean
@ -48,6 +48,7 @@ const Image = ({
? { ? {
animate, animate,
initial: { opacity: 0 }, initial: { opacity: 0 },
exit: { opacity: 0 },
transition, transition,
} }
: {} : {}
@ -60,22 +61,30 @@ const Image = ({
: {} : {}
return ( return (
<div className={cx('relative overflow-hidden', className)}> <div
className={cx(
'overflow-hidden',
className,
className?.includes('absolute') === false && 'relative'
)}
>
{/* Image */} {/* Image */}
<motion.img <AnimatePresence>
alt={alt} <motion.img
className='absolute inset-0 h-full w-full' alt={alt}
src={src} className='absolute inset-0 h-full w-full'
srcSet={srcSet} src={src}
sizes={sizes} srcSet={srcSet}
decoding='async' sizes={sizes}
loading={lazyLoad ? 'lazy' : undefined} decoding='async'
onError={onError} loading={lazyLoad ? 'lazy' : undefined}
onLoad={onLoad} onError={onError}
onClick={onClick} onLoad={onLoad}
onMouseOver={onMouseOver} onClick={onClick}
{...motionProps} onMouseOver={onMouseOver}
/> {...motionProps}
/>
</AnimatePresence>
{/* Placeholder / Error fallback */} {/* Placeholder / Error fallback */}
<AnimatePresence> <AnimatePresence>

View file

@ -4,6 +4,7 @@ import Router from './Router'
const Main = () => { const Main = () => {
return ( return (
<main <main
id='main'
className={cx( className={cx(
'no-scrollbar overflow-y-auto pb-16 pr-6 pl-10', 'no-scrollbar overflow-y-auto pb-16 pr-6 pl-10',
css` css`

View file

@ -8,6 +8,10 @@ import { AnimatePresence, motion } from 'framer-motion'
import Image from './Image' import Image from './Image'
import Wave from './Wave' import Wave from './Wave'
import Icon from '@/web/components/Icon' import Icon from '@/web/components/Icon'
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef } from 'react'
import { useWindowSize } from 'react-use'
import { playerWidth, topbarHeight } from '@/web/utils/const'
const Header = () => { const Header = () => {
return ( return (
@ -46,13 +50,13 @@ const Track = ({
return ( return (
<motion.div <motion.div
className='flex items-center justify-between' className='flex items-center justify-between'
initial={{ opacity: 0 }} // initial={{ opacity: 0 }}
animate={{ opacity: 1 }} // animate={{ opacity: 1 }}
exit={{ x: '100%', opacity: 0 }} // exit={{ x: '100%', opacity: 0 }}
transition={{ // transition={{
duration: 0.24, // duration: 0.24,
}} // }}
layout // layout
onClick={e => { onClick={e => {
if (e.detail === 2 && track?.id) player.playTrack(track.id) if (e.detail === 2 && track?.id) player.playTrack(track.id)
}} }}
@ -62,6 +66,8 @@ const Track = ({
alt='Cover' alt='Cover'
className='mr-4 aspect-square h-14 w-14 flex-shrink-0 rounded-12' className='mr-4 aspect-square h-14 w-14 flex-shrink-0 rounded-12'
src={resizeImage(track?.al?.picUrl || '', 'sm')} src={resizeImage(track?.al?.picUrl || '', 'sm')}
animation={false}
placeholder={false}
/> />
{/* Track info */} {/* Track info */}
@ -93,41 +99,82 @@ const Track = ({
) )
} }
const PlayingNext = ({ className }: { className?: string }) => { const TrackList = ({ className }: { className?: string }) => {
const { trackList, trackIndex, state } = useSnapshot(player) const { trackList, trackIndex, state } = useSnapshot(player)
const { data: tracks } = useTracks({ ids: trackList }) const { data: tracksRaw } = useTracks({ ids: trackList })
const tracks = tracksRaw?.songs || []
const parentRef = useRef<HTMLDivElement>(null)
const { height } = useWindowSize()
const listHeight = height - topbarHeight - playerWidth - 24 - 20 // 24是封面与底部间距20是list与封面间距
const rowVirtualizer = useVirtualizer({
count: tracks.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 76,
overscan: 5,
})
return ( return (
<> <>
<Header />
<div <div
ref={parentRef}
style={{
height: `${listHeight}px`,
}}
className={cx( className={cx(
'no-scrollbar relative z-10 overflow-scroll', 'no-scrollbar relative z-10 w-full overflow-auto',
className, className,
css` css`
padding-top: 42px; padding-top: 42px;
mask-image: linear-gradient(to bottom, transparent 0, black 42px); mask-image: linear-gradient(
to bottom,
transparent 0,
black 42px
); // 顶部渐变遮罩
` `
)} )}
> >
<motion.div className='grid gap-4'> <div
<AnimatePresence> className='relative w-full'
{tracks?.songs?.map((track, index) => ( style={{
height: `${rowVirtualizer.getTotalSize()}px`,
}}
>
{rowVirtualizer.getVirtualItems().map((row: any) => (
<div
key={row.index}
className='absolute top-0 left-0 w-full'
style={{
height: `${row.size}px`,
transform: `translateY(${row.start}px)`,
}}
>
<Track <Track
key={track.id} track={tracks?.[row.index]}
track={track} index={row.index}
index={index}
playingTrackIndex={trackIndex} playingTrackIndex={trackIndex}
state={state} state={state}
/> />
))} </div>
))}
{(tracks?.songs?.length || 0) >= 4 && ( </div>
<div className='pointer-events-none sticky bottom-0 h-8 w-full bg-gradient-to-t from-black'></div>
)}
</AnimatePresence>
</motion.div>
</div> </div>
{/* 底部渐变遮罩 */}
<div
className='pointer-events-none absolute right-0 left-0 z-20 h-14 bg-gradient-to-t from-black to-transparent'
style={{ top: `${listHeight - 56}px` }}
></div>
</>
)
}
const PlayingNext = ({ className }: { className?: string }) => {
return (
<>
<Header />
<TrackList className={className} />
</> </>
) )
} }

View file

@ -1,10 +1,134 @@
import { formatDate, formatDuration, resizeImage } from '@/web/utils/common' import {
formatDate,
formatDuration,
isIOS,
isSafari,
resizeImage,
} from '@/web/utils/common'
import { css, cx } from '@emotion/css' import { css, cx } from '@emotion/css'
import Icon from '@/web/components/Icon' import Icon from '@/web/components/Icon'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useMemo } from 'react'
import Image from './Image' import Image from './Image'
import useIsMobile from '@/web/hooks/useIsMobile' import useIsMobile from '@/web/hooks/useIsMobile'
import { memo, useEffect, useMemo, useRef } from 'react'
import Hls from 'hls.js'
import Plyr, { APITypes, PlyrProps, PlyrInstance } from 'plyr-react'
import useVideoCover from '@/web/hooks/useVideoCover'
import { motion } from 'framer-motion'
import { ease } from '@/web/utils/const'
import { injectGlobal } from '@emotion/css'
injectGlobal`
.plyr__video-wrapper,
.plyr--video {
background-color: transparent !important;
}
`
const VideoCover = ({ source }: { source?: string }) => {
const ref = useRef<APITypes>(null)
useEffect(() => {
const loadVideo = async () => {
if (!source || !Hls.isSupported()) return
const video = document.getElementById('plyr') as HTMLVideoElement
const hls = new Hls()
hls.loadSource(source)
hls.attachMedia(video)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
ref.current!.plyr.media = video
hls.on(Hls.Events.MANIFEST_PARSED, () => {
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;(ref.current!.plyr as PlyrInstance).play()
})
}
loadVideo()
})
return (
<div className='z-10 aspect-square overflow-hidden rounded-24'>
<Plyr
id='plyr'
options={{
volume: 0,
controls: [],
autoplay: true,
clickToPlay: false,
loop: {
active: true,
},
}}
source={{} as PlyrProps['source']}
ref={ref}
/>
</div>
)
}
const Cover = memo(
({ album, playlist }: { album?: Album; playlist?: Playlist }) => {
const isMobile = useIsMobile()
const { data: videoCover } = useVideoCover({
id: album?.id,
name: album?.name,
artist: album?.artist.name,
})
const cover = album?.picUrl || playlist?.coverImgUrl || ''
return (
<>
<div className='relative z-10 aspect-square w-full overflow-auto rounded-24 '>
<Image
className='absolute inset-0 h-full w-full'
src={resizeImage(cover, 'lg')}
alt='Cover'
/>
{videoCover && (
<motion.div
initial={{ opacity: isIOS ? 1 : 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, ease }}
className='absolute inset-0 h-full w-full'
>
{isSafari ? (
<video
src={videoCover}
className='h-full w-full'
autoPlay
loop
muted
playsInline
></video>
) : (
<VideoCover source={videoCover} />
)}
</motion.div>
)}
</div>
{/* Blur bg */}
{!isMobile && (
<img
className={cx(
'absolute z-0 object-cover opacity-70',
css`
top: -400px;
left: -370px;
width: 1572px;
height: 528px;
filter: blur(256px) saturate(1.2);
`
)}
src={resizeImage(cover, 'sm')}
/>
)}
</>
)
}
)
Cover.displayName = 'Cover'
const TrackListHeader = ({ const TrackListHeader = ({
album, album,
@ -15,18 +139,16 @@ const TrackListHeader = ({
playlist?: Playlist playlist?: Playlist
onPlay: () => void onPlay: () => void
}) => { }) => {
const isMobile = useIsMobile()
const albumDuration = useMemo(() => { const albumDuration = useMemo(() => {
const duration = album?.songs?.reduce((acc, cur) => acc + cur.dt, 0) || 0 const duration = album?.songs?.reduce((acc, cur) => acc + cur.dt, 0) || 0
return formatDuration(duration, 'en', 'hh[hr] mm[min]') return formatDuration(duration, 'en', 'hh[hr] mm[min]')
}, [album?.songs]) }, [album?.songs])
const isMobile = useIsMobile()
const cover = album?.picUrl || playlist?.coverImgUrl || ''
return ( return (
<div <div
className={cx( className={cx(
'mx-2.5 rounded-48 p-8 dark:bg-white/10', 'z-10 mx-2.5 rounded-48 p-8 dark:bg-white/10',
'lg:mx-0 lg:grid lg:grid-rows-1 lg:gap-10 lg:rounded-none lg:p-0 lg:dark:bg-transparent', 'lg:mx-0 lg:grid lg:grid-rows-1 lg:gap-10 lg:rounded-none lg:p-0 lg:dark:bg-transparent',
!isMobile && !isMobile &&
css` css`
@ -34,29 +156,7 @@ const TrackListHeader = ({
` `
)} )}
> >
{/* Cover */} <Cover {...{ album, playlist }} />
<Image
className='z-10 aspect-square w-full rounded-24'
src={resizeImage(cover, 'lg')}
alt='Cover'
/>
{/* Blur bg */}
{!isMobile && (
<img
className={cx(
'absolute z-0 object-cover opacity-70',
css`
top: -400px;
left: -370px;
width: 1572px;
height: 528px;
filter: blur(256px) saturate(1.2);
`
)}
src={resizeImage(cover, 'sm')}
/>
)}
<div className='flex flex-col justify-between'> <div className='flex flex-col justify-between'>
<div> <div>

View file

@ -10,6 +10,10 @@ declare global {
channel: T, channel: T,
params?: IpcChannelsParams[T] params?: IpcChannelsParams[T]
) => IpcChannelsReturns[T] ) => IpcChannelsReturns[T]
invoke: <T extends keyof IpcChannelsParams>(
channel: T,
params?: IpcChannelsParams[T]
) => Promise<IpcChannelsReturns[T]>
send: <T extends keyof IpcChannelsParams>( send: <T extends keyof IpcChannelsParams>(
channel: T, channel: T,
params?: IpcChannelsParams[T] params?: IpcChannelsParams[T]

View file

@ -0,0 +1,43 @@
import { IpcChannels } from '@/shared/IpcChannels'
import axios from 'axios'
import { useQuery } from 'react-query'
export default function useVideoCover(props: {
id?: number
name?: string
artist?: string
}) {
const { id, name, artist } = props
return useQuery(
['useVideoCover', props],
async () => {
if (!id || !name || !artist) return
const fromCache = window.ipcRenderer?.sendSync(
IpcChannels.GetVideoCover,
{
id,
}
)
if (fromCache) {
return fromCache === 'no' ? undefined : fromCache
}
const fromRemote = await axios.get('/yesplaymusic/video-cover', {
params: props,
})
window.ipcRenderer?.send(IpcChannels.SetVideoCover, {
id,
url: fromRemote.data.url || '',
})
if (fromRemote?.data?.url) {
return fromRemote.data.url
}
},
{
enabled: !!id && !!name && !!artist,
refetchOnWindowFocus: false,
refetchInterval: false,
}
)
}

View file

@ -5,7 +5,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/public/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/src/public/favicon.svg" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' www.googletagmanager.com;" /> <meta http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline' www.googletagmanager.com blob:;" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<title>YesPlayMusic</title> <title>YesPlayMusic</title>

View file

@ -14,6 +14,7 @@ import ReactGA from 'react-ga4'
import { ipcRenderer } from './ipcRenderer' import { ipcRenderer } from './ipcRenderer'
import { QueryClientProvider } from 'react-query' import { QueryClientProvider } from 'react-query'
import reactQueryClient from '@/web/utils/reactQueryClient' import reactQueryClient from '@/web/utils/reactQueryClient'
import ReactDOM from 'react-dom'
ReactGA.initialize('G-KMJJCFZDKF') ReactGA.initialize('G-KMJJCFZDKF')
@ -34,12 +35,23 @@ ipcRenderer()
const container = document.getElementById('root') as HTMLElement const container = document.getElementById('root') as HTMLElement
const root = ReactDOMClient.createRoot(container) const root = ReactDOMClient.createRoot(container)
root.render( // root.render(
// <StrictMode>
// <BrowserRouter>
// <QueryClientProvider client={reactQueryClient}>
// <App />
// </QueryClientProvider>
// </BrowserRouter>
// </StrictMode>
// )
ReactDOM.render(
<StrictMode> <StrictMode>
<BrowserRouter> <BrowserRouter>
<QueryClientProvider client={reactQueryClient}> <QueryClientProvider client={reactQueryClient}>
<App /> <App />
</QueryClientProvider> </QueryClientProvider>
</BrowserRouter> </BrowserRouter>
</StrictMode> </StrictMode>,
document.getElementById('root')
) )

View file

@ -25,15 +25,19 @@
"@emotion/css": "^11.9.0", "@emotion/css": "^11.9.0",
"@sentry/react": "^6.19.7", "@sentry/react": "^6.19.7",
"@sentry/tracing": "^6.19.7", "@sentry/tracing": "^6.19.7",
"@tanstack/react-virtual": "3.0.0-beta.2",
"axios": "^0.27.2", "axios": "^0.27.2",
"color.js": "^1.2.0", "color.js": "^1.2.0",
"colord": "^2.9.2", "colord": "^2.9.2",
"dayjs": "^1.11.1", "dayjs": "^1.11.1",
"framer-motion": "^6.3.4", "framer-motion": "^6.3.4",
"hls.js": "^1.1.5",
"howler": "^2.2.3", "howler": "^2.2.3",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"md5": "^2.3.0", "md5": "^2.3.0",
"plyr": "^3.7.2",
"plyr-react": "^5.0.2",
"qrcode": "^1.5.0", "qrcode": "^1.5.0",
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",

View file

@ -11,6 +11,7 @@ import useArtistAlbums from '@/web/api/hooks/useArtistAlbums'
import { css, cx } from '@emotion/css' import { css, cx } from '@emotion/css'
import CoverRow from '@/web/components/New/CoverRow' import CoverRow from '@/web/components/New/CoverRow'
import { useMemo } from 'react' import { useMemo } from 'react'
import 'plyr-react/plyr.css'
const MoreByArtist = ({ album }: { album?: Album }) => { const MoreByArtist = ({ album }: { album?: Album }) => {
const { data: albums } = useArtistAlbums({ const { data: albums } = useArtistAlbums({

View file

@ -39,7 +39,10 @@ const Albums = () => {
const Playlists = () => { const Playlists = () => {
const { data: playlists } = useUserPlaylists() const { data: playlists } = useUserPlaylists()
return ( return (
<CoverRow playlists={playlists?.playlist} className='mt-6 px-2.5 lg:px-0' /> <CoverRow
playlists={playlists?.playlist?.slice(1)}
className='mt-6 px-2.5 lg:px-0'
/>
) )
} }

View file

@ -160,7 +160,9 @@ export async function calcCoverColor(coverUrl: string) {
} }
export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) export const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
export const isSafari = /Safari/.test(navigator.userAgent) export const isSafari = /^((?!chrome|android).)*safari/i.test(
navigator.userAgent
)
export const isPWA = export const isPWA =
(navigator as any).standalone || (navigator as any).standalone ||
window.matchMedia('(display-mode: standalone)').matches window.matchMedia('(display-mode: standalone)').matches

View file

@ -1,4 +1,7 @@
// 动画曲线
export const ease: [number, number, number, number] = [0.4, 0, 0.2, 1] export const ease: [number, number, number, number] = [0.4, 0, 0.2, 1]
// 屏幕断点
export const breakpoint = { export const breakpoint = {
sm: '@media (min-width: 640px)', sm: '@media (min-width: 640px)',
md: '@media (min-width: 768px)', md: '@media (min-width: 768px)',
@ -6,3 +9,6 @@ export const breakpoint = {
xl: '@media (min-width: 1280px)', xl: '@media (min-width: 1280px)',
'2xl': '@media (min-width: 1536px)', '2xl': '@media (min-width: 1536px)',
} }
export const topbarHeight = 132 // 桌面端顶栏高度 (px)
export const playerWidth = 318 // 桌面端播放器宽度 (px)

View file

@ -91,6 +91,11 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
rewrite: path => (IS_ELECTRON ? path : path.replace(/^\/netease/, '')), rewrite: path => (IS_ELECTRON ? path : path.replace(/^\/netease/, '')),
}, },
'/yesplaymusic/video-cover': {
target: `http://168.138.40.199:51324`,
// target: `http://127.0.0.1:51324`,
changeOrigin: true,
},
'/yesplaymusic/': { '/yesplaymusic/': {
target: `http://127.0.0.1:${ target: `http://127.0.0.1:${
process.env.ELECTRON_DEV_NETEASE_API_PORT || 3000 process.env.ELECTRON_DEV_NETEASE_API_PORT || 3000

107
pnpm-lock.yaml generated
View file

@ -48,6 +48,7 @@ importers:
express: ^4.18.1 express: ^4.18.1
express-fileupload: ^1.4.0 express-fileupload: ^1.4.0
fast-folder-size: ^1.7.0 fast-folder-size: ^1.7.0
m3u8-parser: ^4.7.1
minimist: ^1.2.6 minimist: ^1.2.6
music-metadata: ^7.12.3 music-metadata: ^7.12.3
NeteaseCloudMusicApi: ^4.6.2 NeteaseCloudMusicApi: ^4.6.2
@ -70,6 +71,7 @@ importers:
electron-store: 8.0.1 electron-store: 8.0.1
express: 4.18.1 express: 4.18.1
fast-folder-size: 1.7.0 fast-folder-size: 1.7.0
m3u8-parser: 4.7.1
NeteaseCloudMusicApi: 4.6.2 NeteaseCloudMusicApi: 4.6.2
pretty-bytes: 6.0.0 pretty-bytes: 6.0.0
devDependencies: devDependencies:
@ -115,6 +117,7 @@ importers:
'@storybook/builder-vite': ^0.1.35 '@storybook/builder-vite': ^0.1.35
'@storybook/react': ^6.5.5 '@storybook/react': ^6.5.5
'@storybook/testing-library': ^0.0.11 '@storybook/testing-library': ^0.0.11
'@tanstack/react-virtual': 3.0.0-beta.2
'@testing-library/react': ^13.3.0 '@testing-library/react': ^13.3.0
'@types/howler': ^2.2.7 '@types/howler': ^2.2.7
'@types/js-cookie': ^3.0.2 '@types/js-cookie': ^3.0.2
@ -138,12 +141,15 @@ importers:
eslint-plugin-react: ^7.30.0 eslint-plugin-react: ^7.30.0
eslint-plugin-react-hooks: ^4.5.0 eslint-plugin-react-hooks: ^4.5.0
framer-motion: ^6.3.4 framer-motion: ^6.3.4
hls.js: ^1.1.5
howler: ^2.2.3 howler: ^2.2.3
js-cookie: ^3.0.1 js-cookie: ^3.0.1
jsdom: ^19.0.0 jsdom: ^19.0.0
lodash-es: ^4.17.21 lodash-es: ^4.17.21
md5: ^2.3.0 md5: ^2.3.0
open-cli: ^7.0.1 open-cli: ^7.0.1
plyr: ^3.7.2
plyr-react: ^5.0.2
postcss: ^8.4.14 postcss: ^8.4.14
prettier: '*' prettier: '*'
prettier-plugin-tailwindcss: ^0.1.11 prettier-plugin-tailwindcss: ^0.1.11
@ -169,15 +175,19 @@ importers:
'@emotion/css': 11.9.0 '@emotion/css': 11.9.0
'@sentry/react': 6.19.7_react@18.1.0 '@sentry/react': 6.19.7_react@18.1.0
'@sentry/tracing': 6.19.7 '@sentry/tracing': 6.19.7
'@tanstack/react-virtual': 3.0.0-beta.2
axios: 0.27.2 axios: 0.27.2
color.js: 1.2.0 color.js: 1.2.0
colord: 2.9.2 colord: 2.9.2
dayjs: 1.11.2 dayjs: 1.11.2
framer-motion: 6.3.10_ef5jwxihqo6n7gxfmzogljlgcm framer-motion: 6.3.10_ef5jwxihqo6n7gxfmzogljlgcm
hls.js: 1.1.5
howler: 2.2.3 howler: 2.2.3
js-cookie: 3.0.1 js-cookie: 3.0.1
lodash-es: 4.17.21 lodash-es: 4.17.21
md5: 2.3.0 md5: 2.3.0
plyr: 3.7.2
plyr-react: 5.0.2_react@18.1.0
qrcode: 1.5.0 qrcode: 1.5.0
react: 18.1.0 react: 18.1.0
react-dom: 18.1.0_react@18.1.0 react-dom: 18.1.0_react@18.1.0
@ -4527,6 +4537,10 @@ packages:
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
dev: true dev: true
/@reach/observe-rect/1.2.0:
resolution: {integrity: sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==}
dev: false
/@rollup/plugin-babel/5.3.1_4kojsos35jimftt7mhjohcqk6y: /@rollup/plugin-babel/5.3.1_4kojsos35jimftt7mhjohcqk6y:
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
@ -6386,6 +6400,13 @@ packages:
defer-to-connect: 2.0.1 defer-to-connect: 2.0.1
dev: true dev: true
/@tanstack/react-virtual/3.0.0-beta.2:
resolution: {integrity: sha512-pwA9URTHYXX/2PgIISoMcf1P77hxf5oI3L/IDQ19Q1xuAc76o2R2CwHv6vvl5fDhwVj5klOfBxJvuT61Lhy9/w==}
engines: {node: '>=12'}
dependencies:
'@reach/observe-rect': 1.2.0
dev: false
/@testing-library/dom/8.13.0: /@testing-library/dom/8.13.0:
resolution: {integrity: sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==} resolution: {integrity: sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -7119,6 +7140,15 @@ packages:
'@unblockneteasemusic/rust-napi-win32-x64-msvc': 0.3.0 '@unblockneteasemusic/rust-napi-win32-x64-msvc': 0.3.0
dev: false dev: false
/@videojs/vhs-utils/3.0.5:
resolution: {integrity: sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==}
engines: {node: '>=8', npm: '>=5'}
dependencies:
'@babel/runtime': 7.18.3
global: 4.4.0
url-toolkit: 2.2.5
dev: false
/@vitejs/plugin-react/1.3.2: /@vitejs/plugin-react/1.3.2:
resolution: {integrity: sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==} resolution: {integrity: sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@ -9424,7 +9454,6 @@ packages:
/core-js/3.22.7: /core-js/3.22.7:
resolution: {integrity: sha512-Jt8SReuDKVNZnZEzyEQT5eK6T2RRCXkfTq7Lo09kpm+fHjgGewSbNjV+Wt4yZMhPDdzz2x1ulI5z/w4nxpBseg==} resolution: {integrity: sha512-Jt8SReuDKVNZnZEzyEQT5eK6T2RRCXkfTq7Lo09kpm+fHjgGewSbNjV+Wt4yZMhPDdzz2x1ulI5z/w4nxpBseg==}
requiresBuild: true requiresBuild: true
dev: true
/core-util-is/1.0.2: /core-util-is/1.0.2:
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
@ -9698,6 +9727,10 @@ packages:
dev: true dev: true
optional: true optional: true
/custom-event-polyfill/1.0.7:
resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==}
dev: false
/cyclist/1.0.1: /cyclist/1.0.1:
resolution: {integrity: sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==} resolution: {integrity: sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==}
dev: true dev: true
@ -10109,7 +10142,6 @@ packages:
/dom-walk/0.1.2: /dom-walk/0.1.2:
resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==}
dev: true
/domain-browser/1.2.0: /domain-browser/1.2.0:
resolution: {integrity: sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==} resolution: {integrity: sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==}
@ -12259,7 +12291,6 @@ packages:
dependencies: dependencies:
min-document: 2.19.0 min-document: 2.19.0
process: 0.11.10 process: 0.11.10
dev: true
/globals/11.12.0: /globals/11.12.0:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
@ -12578,6 +12609,10 @@ packages:
dependencies: dependencies:
'@babel/runtime': 7.18.3 '@babel/runtime': 7.18.3
/hls.js/1.1.5:
resolution: {integrity: sha512-mQX5TSNtJEzGo5HPpvcQgCu+BWoKDQM6YYtg/KbgWkmVAcqOCvSTi0SuqG2ZJLXxIzdnFcKU2z7Mrw/YQWhPOA==}
dev: false
/hmac-drbg/1.0.1: /hmac-drbg/1.0.1:
resolution: {integrity: sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=} resolution: {integrity: sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=}
dependencies: dependencies:
@ -13861,6 +13896,10 @@ packages:
json5: 2.2.1 json5: 2.2.1
dev: true dev: true
/loadjs/4.2.0:
resolution: {integrity: sha512-AgQGZisAlTPbTEzrHPb6q+NYBMD+DP9uvGSIjSUM5uG+0jG15cb8axWpxuOIqrmQjn6scaaH8JwloiP27b2KXA==}
dev: false
/local-pkg/0.4.1: /local-pkg/0.4.1:
resolution: {integrity: sha512-lL87ytIGP2FU5PWwNDo0w3WhIo2gopIAxPg9RxDYF7m4rr5ahuZxP22xnJHIvaLTe4Z9P6uKKY2UHiwyB4pcrw==} resolution: {integrity: sha512-lL87ytIGP2FU5PWwNDo0w3WhIo2gopIAxPg9RxDYF7m4rr5ahuZxP22xnJHIvaLTe4Z9P6uKKY2UHiwyB4pcrw==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -14001,6 +14040,14 @@ packages:
readable-stream: 3.6.0 readable-stream: 3.6.0
dev: true dev: true
/m3u8-parser/4.7.1:
resolution: {integrity: sha512-pbrQwiMiq+MmI9bl7UjtPT3AK603PV9bogNlr83uC+X9IoxqL5E4k7kU7fMQ0dpRgxgeSMygqUa0IMLQNXLBNA==}
dependencies:
'@babel/runtime': 7.18.3
'@videojs/vhs-utils': 3.0.5
global: 4.4.0
dev: false
/magic-string/0.25.9: /magic-string/0.25.9:
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
dependencies: dependencies:
@ -14357,10 +14404,9 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
/min-document/2.19.0: /min-document/2.19.0:
resolution: {integrity: sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=} resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==}
dependencies: dependencies:
dom-walk: 0.1.2 dom-walk: 0.1.2
dev: true
/min-indent/1.0.1: /min-indent/1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
@ -15472,6 +15518,30 @@ packages:
xmlbuilder: 9.0.7 xmlbuilder: 9.0.7
dev: true dev: true
/plyr-react/5.0.2_react@18.1.0:
resolution: {integrity: sha512-CksykyesFtmPoslasOVIplYZkduJ2OQ/q3QNUdktjy8Ds4Rhxw9u57jKBjSLdpyhENp/Yu+lDC7lOHc1o9iPUQ==}
engines: {node: '>=12.7.0'}
peerDependencies:
react: '>=16.8'
peerDependenciesMeta:
react:
optional: true
dependencies:
plyr: 3.7.2
react: 18.1.0
react-aptor: 2.0.0-alpha.1_react@18.1.0
dev: false
/plyr/3.7.2:
resolution: {integrity: sha512-I0ZC/OI4oJ0iWG9s2rrnO0YFO6aLyrPiQBq9kum0FqITYljwTPBbYL3TZZu8UJQJUq7tUWN18Q7ACwNCkGKABQ==}
dependencies:
core-js: 3.22.7
custom-event-polyfill: 1.0.7
loadjs: 4.2.0
rangetouch: 2.0.1
url-polyfill: 1.1.12
dev: false
/pngjs/5.0.0: /pngjs/5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@ -15793,9 +15863,8 @@ packages:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
/process/0.11.10: /process/0.11.10:
resolution: {integrity: sha1-czIwDoQBYb2j5podHZGn1LwW8YI=} resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'} engines: {node: '>= 0.6.0'}
dev: true
/progress/2.0.3: /progress/2.0.3:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
@ -16020,6 +16089,10 @@ packages:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
/rangetouch/2.0.1:
resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==}
dev: false
/raw-body/2.5.1: /raw-body/2.5.1:
resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -16049,6 +16122,18 @@ packages:
minimist: 1.2.6 minimist: 1.2.6
strip-json-comments: 2.0.1 strip-json-comments: 2.0.1
/react-aptor/2.0.0-alpha.1_react@18.1.0:
resolution: {integrity: sha512-FbvxQKsZMUZcLr2WdrQEmxH0kifsN4N+v6YdL1g3At03zouJCEcPXv+o+bhP3Ci3ya4QPvNHK/bpbrCzuKWOMw==}
engines: {node: '>=12.7.0'}
peerDependencies:
react: '>=16.8'
peerDependenciesMeta:
react:
optional: true
dependencies:
react: 18.1.0
dev: false
/react-docgen-typescript/2.2.2_typescript@4.7.3: /react-docgen-typescript/2.2.2_typescript@4.7.3:
resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==}
peerDependencies: peerDependencies:
@ -18653,6 +18738,14 @@ packages:
prepend-http: 2.0.0 prepend-http: 2.0.0
dev: true dev: true
/url-polyfill/1.1.12:
resolution: {integrity: sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A==}
dev: false
/url-toolkit/2.2.5:
resolution: {integrity: sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==}
dev: false
/url/0.11.0: /url/0.11.0:
resolution: {integrity: sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=} resolution: {integrity: sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=}
dependencies: dependencies: