Compare commits
2 Commits
master
...
feat-twitc
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f8510bac8 | |||
| 4e1b4bad0a |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yt-dlp-webui",
|
"name": "yt-dlp-webui",
|
||||||
"version": "3.2.6",
|
"version": "3.2.5",
|
||||||
"description": "Frontend compontent of yt-dlp-webui",
|
"description": "Frontend compontent of yt-dlp-webui",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
@@ -18,11 +18,11 @@
|
|||||||
"@mui/icons-material": "^6.2.0",
|
"@mui/icons-material": "^6.2.0",
|
||||||
"@mui/material": "^6.2.0",
|
"@mui/material": "^6.2.0",
|
||||||
"fp-ts": "^2.16.5",
|
"fp-ts": "^2.16.5",
|
||||||
"jotai": "^2.10.3",
|
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^6.23.1",
|
"react-router-dom": "^6.23.1",
|
||||||
"react-virtuoso": "^4.7.11",
|
"react-virtuoso": "^4.7.11",
|
||||||
|
"jotai": "^2.10.3",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import Footer from './components/Footer'
|
|||||||
import Logout from './components/Logout'
|
import Logout from './components/Logout'
|
||||||
import SocketSubscriber from './components/SocketSubscriber'
|
import SocketSubscriber from './components/SocketSubscriber'
|
||||||
import ThemeToggler from './components/ThemeToggler'
|
import ThemeToggler from './components/ThemeToggler'
|
||||||
import TwitchIcon from './components/TwitchIcon'
|
|
||||||
import { useI18n } from './hooks/useI18n'
|
import { useI18n } from './hooks/useI18n'
|
||||||
import Toaster from './providers/ToasterProvider'
|
import Toaster from './providers/ToasterProvider'
|
||||||
import { getAccentValue } from './utils'
|
import { getAccentValue } from './utils'
|
||||||
@@ -155,19 +154,6 @@ export default function Layout() {
|
|||||||
<ListItemText primary={i18n.t('subscriptionsButtonLabel')} />
|
<ListItemText primary={i18n.t('subscriptionsButtonLabel')} />
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to={'/twitch'} style={
|
|
||||||
{
|
|
||||||
textDecoration: 'none',
|
|
||||||
color: mode === 'dark' ? '#ffffff' : '#000000DE'
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
<ListItemButton>
|
|
||||||
<ListItemIcon>
|
|
||||||
<TwitchIcon />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary={"Twitch"} />
|
|
||||||
</ListItemButton>
|
|
||||||
</Link>
|
|
||||||
<Link to={'/monitor'} style={
|
<Link to={'/monitor'} style={
|
||||||
{
|
{
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
|
|||||||
@@ -80,7 +80,4 @@ keys:
|
|||||||
cronExpressionLabel: 'Cron expression'
|
cronExpressionLabel: 'Cron expression'
|
||||||
editButtonLabel: 'Edit'
|
editButtonLabel: 'Edit'
|
||||||
newSubscriptionButton: New subscription
|
newSubscriptionButton: New subscription
|
||||||
clearCompletedButton: 'Clear completed'
|
clearCompletedButton: 'Clear completed'
|
||||||
twitchIntegrationInfo: |
|
|
||||||
To enable monitoring Twitch streams follow this wiki page.
|
|
||||||
https://github.com/marcopiovanello/yt-dlp-web-ui/wiki/Twitch-integration
|
|
||||||
@@ -32,6 +32,7 @@ const HomeSpeedDial: React.FC<Props> = ({ onDownloadOpen, onEditorOpen }) => {
|
|||||||
ariaLabel="Home speed dial"
|
ariaLabel="Home speed dial"
|
||||||
sx={{ position: 'absolute', bottom: 64, right: 24 }}
|
sx={{ position: 'absolute', bottom: 64, right: 24 }}
|
||||||
icon={<SpeedDialIcon />}
|
icon={<SpeedDialIcon />}
|
||||||
|
onClick={onDownloadOpen}
|
||||||
>
|
>
|
||||||
<SpeedDialAction
|
<SpeedDialAction
|
||||||
icon={listView ? <ViewAgendaIcon /> : <FormatListBulleted />}
|
icon={listView ? <ViewAgendaIcon /> : <FormatListBulleted />}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { useAtomValue } from 'jotai'
|
|
||||||
import { settingsState } from '../atoms/settings'
|
|
||||||
|
|
||||||
const TwitchIcon: React.FC = () => {
|
|
||||||
const { theme } = useAtomValue(settingsState)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
style={{ fill: theme === 'dark' ? '#fff' : '#757575' }}
|
|
||||||
>
|
|
||||||
<title>Twitch</title>
|
|
||||||
<path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TwitchIcon
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
import CloseIcon from '@mui/icons-material/Close'
|
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
AppBar,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
Dialog,
|
|
||||||
Grid,
|
|
||||||
IconButton,
|
|
||||||
Paper,
|
|
||||||
Slide,
|
|
||||||
TextField,
|
|
||||||
Toolbar,
|
|
||||||
Typography
|
|
||||||
} from '@mui/material'
|
|
||||||
import { TransitionProps } from '@mui/material/transitions'
|
|
||||||
import { matchW } from 'fp-ts/lib/Either'
|
|
||||||
import { pipe } from 'fp-ts/lib/function'
|
|
||||||
import { useAtomValue } from 'jotai'
|
|
||||||
import { forwardRef, startTransition, useState } from 'react'
|
|
||||||
import { serverURL } from '../../atoms/settings'
|
|
||||||
import { useToast } from '../../hooks/toast'
|
|
||||||
import { useI18n } from '../../hooks/useI18n'
|
|
||||||
import { ffetch } from '../../lib/httpClient'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
open: boolean
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const Transition = forwardRef(function Transition(
|
|
||||||
props: TransitionProps & {
|
|
||||||
children: React.ReactElement
|
|
||||||
},
|
|
||||||
ref: React.Ref<unknown>,
|
|
||||||
) {
|
|
||||||
return <Slide direction="up" ref={ref} {...props} />
|
|
||||||
})
|
|
||||||
|
|
||||||
const TwitchDialog: React.FC<Props> = ({ open, onClose }) => {
|
|
||||||
const [channelURL, setChannelURL] = useState('')
|
|
||||||
|
|
||||||
const { i18n } = useI18n()
|
|
||||||
const { pushMessage } = useToast()
|
|
||||||
|
|
||||||
const baseURL = useAtomValue(serverURL)
|
|
||||||
|
|
||||||
const submit = async (channelURL: string) => {
|
|
||||||
const task = ffetch<void>(`${baseURL}/twitch/user`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
user: channelURL.split('/').at(-1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const either = await task()
|
|
||||||
|
|
||||||
pipe(
|
|
||||||
either,
|
|
||||||
matchW(
|
|
||||||
(l) => pushMessage(l, 'error'),
|
|
||||||
(_) => onClose()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
fullScreen
|
|
||||||
open={open}
|
|
||||||
onClose={onClose}
|
|
||||||
TransitionComponent={Transition}
|
|
||||||
>
|
|
||||||
<AppBar sx={{ position: 'relative' }}>
|
|
||||||
<Toolbar>
|
|
||||||
<IconButton
|
|
||||||
edge="start"
|
|
||||||
color="inherit"
|
|
||||||
onClick={onClose}
|
|
||||||
aria-label="close"
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
|
|
||||||
{i18n.t('subscriptionsButtonLabel')}
|
|
||||||
</Typography>
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
<Box sx={{
|
|
||||||
backgroundColor: (theme) => theme.palette.background.default,
|
|
||||||
minHeight: (theme) => `calc(99vh - ${theme.mixins.toolbar.minHeight}px)`
|
|
||||||
}}>
|
|
||||||
<Container sx={{ my: 4 }}>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Paper
|
|
||||||
elevation={4}
|
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Grid container gap={1.5}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Alert severity="info">
|
|
||||||
{i18n.t('twitchIntegrationInfo')}
|
|
||||||
</Alert>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} mt={1}>
|
|
||||||
<TextField
|
|
||||||
multiline
|
|
||||||
fullWidth
|
|
||||||
label={i18n.t('subscriptionsURLInput')}
|
|
||||||
variant="outlined"
|
|
||||||
placeholder="https://www.twitch.tv/a_twitch_user_that_exists"
|
|
||||||
onChange={(e) => setChannelURL(e.target.value)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Button
|
|
||||||
sx={{ mt: 2 }}
|
|
||||||
variant="contained"
|
|
||||||
disabled={channelURL === ''}
|
|
||||||
onClick={() => startTransition(() => submit(channelURL))}
|
|
||||||
>
|
|
||||||
{i18n.t('startButton')}
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Container>
|
|
||||||
</Box>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TwitchDialog
|
|
||||||
@@ -6,7 +6,6 @@ import Terminal from './views/Terminal'
|
|||||||
|
|
||||||
const Home = lazy(() => import('./views/Home'))
|
const Home = lazy(() => import('./views/Home'))
|
||||||
const Login = lazy(() => import('./views/Login'))
|
const Login = lazy(() => import('./views/Login'))
|
||||||
const Twitch = lazy(() => import('./views/Twitch'))
|
|
||||||
const Archive = lazy(() => import('./views/Archive'))
|
const Archive = lazy(() => import('./views/Archive'))
|
||||||
const Settings = lazy(() => import('./views/Settings'))
|
const Settings = lazy(() => import('./views/Settings'))
|
||||||
const LiveStream = lazy(() => import('./views/Livestream'))
|
const LiveStream = lazy(() => import('./views/Livestream'))
|
||||||
@@ -112,14 +111,6 @@ export const router = createHashRouter([
|
|||||||
</Suspense >
|
</Suspense >
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/twitch',
|
|
||||||
element: (
|
|
||||||
<Suspense fallback={<CircularProgress />}>
|
|
||||||
<Twitch />
|
|
||||||
</Suspense >
|
|
||||||
)
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import {
|
|
||||||
Chip,
|
|
||||||
Container,
|
|
||||||
Paper
|
|
||||||
} from '@mui/material'
|
|
||||||
import { matchW } from 'fp-ts/lib/Either'
|
|
||||||
import { pipe } from 'fp-ts/lib/function'
|
|
||||||
import { useAtomValue } from 'jotai'
|
|
||||||
import { useState, useTransition } from 'react'
|
|
||||||
import { serverURL } from '../atoms/settings'
|
|
||||||
import LoadingBackdrop from '../components/LoadingBackdrop'
|
|
||||||
import NoSubscriptions from '../components/subscriptions/NoSubscriptions'
|
|
||||||
import SubscriptionsSpeedDial from '../components/subscriptions/SubscriptionsSpeedDial'
|
|
||||||
import TwitchDialog from '../components/twitch/TwitchDialog'
|
|
||||||
import { useToast } from '../hooks/toast'
|
|
||||||
import useFetch from '../hooks/useFetch'
|
|
||||||
import { ffetch } from '../lib/httpClient'
|
|
||||||
|
|
||||||
const TwitchView: React.FC = () => {
|
|
||||||
const { pushMessage } = useToast()
|
|
||||||
|
|
||||||
const baseURL = useAtomValue(serverURL)
|
|
||||||
|
|
||||||
const [openDialog, setOpenDialog] = useState(false)
|
|
||||||
|
|
||||||
const { data: users, fetcher: refetch } = useFetch<Array<string>>('/twitch/users')
|
|
||||||
|
|
||||||
const [isPending, startTransition] = useTransition()
|
|
||||||
|
|
||||||
const deleteUser = async (user: string) => {
|
|
||||||
const task = ffetch<void>(`${baseURL}/twitch/user/${user}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
const either = await task()
|
|
||||||
|
|
||||||
pipe(
|
|
||||||
either,
|
|
||||||
matchW(
|
|
||||||
(l) => pushMessage(l, 'error'),
|
|
||||||
() => refetch()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LoadingBackdrop isLoading={!users || isPending} />
|
|
||||||
|
|
||||||
<SubscriptionsSpeedDial onOpen={() => setOpenDialog(s => !s)} />
|
|
||||||
|
|
||||||
<TwitchDialog open={openDialog} onClose={() => {
|
|
||||||
setOpenDialog(s => !s)
|
|
||||||
refetch()
|
|
||||||
}} />
|
|
||||||
|
|
||||||
{
|
|
||||||
!users || users.length === 0 ?
|
|
||||||
<NoSubscriptions /> :
|
|
||||||
<Container maxWidth="xl" sx={{ mt: 4, mb: 8 }}>
|
|
||||||
<Paper sx={{
|
|
||||||
p: 2.5,
|
|
||||||
minHeight: '80vh',
|
|
||||||
}}>
|
|
||||||
{users.map(user => (
|
|
||||||
<Chip
|
|
||||||
label={user}
|
|
||||||
onDelete={() => startTransition(async () => await deleteUser(user))}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Paper>
|
|
||||||
</Container>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TwitchView
|
|
||||||
@@ -48,7 +48,6 @@ func Instance() *Config {
|
|||||||
if instance == nil {
|
if instance == nil {
|
||||||
instanceOnce.Do(func() {
|
instanceOnce.Do(func() {
|
||||||
instance = &Config{}
|
instance = &Config{}
|
||||||
instance.Twitch.CheckInterval = time.Minute * 5
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return instance
|
return instance
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ApplyAuthenticationByConfig(next http.Handler) http.Handler {
|
func ApplyAuthenticationByConfig(next http.Handler) http.Handler {
|
||||||
handler := next
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if config.Instance().RequireAuth {
|
||||||
if config.Instance().RequireAuth {
|
Authenticated(next)
|
||||||
handler = Authenticated(handler)
|
}
|
||||||
}
|
if config.Instance().UseOpenId {
|
||||||
if config.Instance().UseOpenId {
|
openid.Middleware(next)
|
||||||
handler = openid.Middleware(handler)
|
}
|
||||||
}
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
return handler
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ func (s *Service) DeleteTemplate(ctx context.Context, id string) error {
|
|||||||
|
|
||||||
func (s *Service) GetVersion(ctx context.Context) (string, string, error) {
|
func (s *Service) GetVersion(ctx context.Context) (string, string, error) {
|
||||||
//TODO: load from realease properties file, or anything else outside code
|
//TODO: load from realease properties file, or anything else outside code
|
||||||
const CURRENT_RPC_VERSION = "3.2.6"
|
const CURRENT_RPC_VERSION = "3.2.5"
|
||||||
|
|
||||||
result := make(chan string, 1)
|
result := make(chan string, 1)
|
||||||
|
|
||||||
|
|||||||
@@ -251,9 +251,8 @@ func newServer(c serverConfig) *http.Server {
|
|||||||
// Twitch
|
// Twitch
|
||||||
r.Route("/twitch", func(r chi.Router) {
|
r.Route("/twitch", func(r chi.Router) {
|
||||||
r.Use(middlewares.ApplyAuthenticationByConfig)
|
r.Use(middlewares.ApplyAuthenticationByConfig)
|
||||||
r.Get("/users", twitch.GetMonitoredUsers(c.tm))
|
r.Get("/all", twitch.GetMonitoredUsers(c.tm))
|
||||||
r.Post("/user", twitch.MonitorUserHandler(c.tm))
|
r.Post("/add", twitch.MonitorUserHandler(c.tm))
|
||||||
r.Delete("/user/{user}", twitch.DeleteUser(c.tm))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return &http.Server{Handler: r}
|
return &http.Server{Handler: r}
|
||||||
|
|||||||
@@ -83,13 +83,6 @@ func (m *Monitor) GetMonitoredUsers() iter.Seq[string] {
|
|||||||
return maps.Keys(m.monitored)
|
return maps.Keys(m.monitored)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Monitor) DeleteUser(user string) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
delete(m.monitored, user)
|
|
||||||
delete(m.lastState, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DEFAULT_DOWNLOAD_HANDLER(db *internal.MemoryDB, mq *internal.MessageQueue) func(url string) error {
|
func DEFAULT_DOWNLOAD_HANDLER(db *internal.MemoryDB, mq *internal.MessageQueue) func(url string) error {
|
||||||
return func(url string) error {
|
return func(url string) error {
|
||||||
p := &internal.Process{
|
p := &internal.Process{
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type addUserReq struct {
|
type addUserReq struct {
|
||||||
@@ -34,30 +32,7 @@ func GetMonitoredUsers(m *Monitor) http.HandlerFunc {
|
|||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
it := m.GetMonitoredUsers()
|
it := m.GetMonitoredUsers()
|
||||||
|
|
||||||
users := slices.Collect(it)
|
if err := json.NewEncoder(w).Encode(slices.Collect(it)); err != nil {
|
||||||
if users == nil {
|
|
||||||
users = make([]string, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(users); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteUser(m *Monitor) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user := chi.URLParam(r, "user")
|
|
||||||
|
|
||||||
if user == "" {
|
|
||||||
http.Error(w, "empty user", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
m.DeleteUser(user)
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode("ok"); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user