+ Twitch{" "}
- VodArchiv.net{" "}
-
+ Live Stream{" "}
+ {" "}
+ and{" "}
+
+ VOD{" "}
+ {" "}
+ archiving platform
- Nahtlose Archivierung von Livestreams und Videoclips.
- Jedes Archiv enthält eine Chat-Wiederholung und einen gerenderten Chat.
+ Seamlessly archive live streams and vods. Each archive includes a
+ chat replay and rendered chat.
@@ -122,7 +133,7 @@ export function LandingHero() {
className={classes.control}
mt={40}
>
- Zu den Kanälen
+ Channels
diff --git a/src/components/Landing/LoggedInHero.tsx b/src/components/Landing/LoggedInHero.tsx
index d9bb673..a6ec110 100644
--- a/src/components/Landing/LoggedInHero.tsx
+++ b/src/components/Landing/LoggedInHero.tsx
@@ -37,7 +37,7 @@ export function LandingLoggedInHero() {
return (
- VodArchiv
+ Ganymede
);
diff --git a/src/components/Landing/Recent.tsx b/src/components/Landing/Recent.tsx
index 2cf6a70..70ef111 100644
--- a/src/components/Landing/Recent.tsx
+++ b/src/components/Landing/Recent.tsx
@@ -39,7 +39,7 @@ const LandingRecentlyArchived = () => {
}),
});
- if (error) return
Fehler beim Laden
;
+ if (error) return
failed to load
;
if (isLoading) return
;
return (
diff --git a/src/components/Playlist/Header.tsx b/src/components/Playlist/Header.tsx
index f5e0bc7..aca6be4 100644
--- a/src/components/Playlist/Header.tsx
+++ b/src/components/Playlist/Header.tsx
@@ -49,7 +49,7 @@ const PlaylistHeader = ({ playlist, handleOpen, handleDeleteOpen }: any) => {
- Einstellungen
+ Settings
handleOpen()}>Edit
handleDeleteOpen()} color="red">
Delete
diff --git a/src/components/Queue/ChatTimeline.tsx b/src/components/Queue/ChatTimeline.tsx
index 6db910f..4daf344 100644
--- a/src/components/Queue/ChatTimeline.tsx
+++ b/src/components/Queue/ChatTimeline.tsx
@@ -59,13 +59,15 @@ const QueueChatTimeline = ({ queue }: Object) => {
>
logs
- {" - "}
- restartTask("chat_download")}
- >
- restart
-
+ {!queue.live_archive && (
+ {" - "}
+ restartTask("chat_download")}
+ >
+ restart
+
+ )}
diff --git a/src/components/Queue/Header.tsx b/src/components/Queue/Header.tsx
index f0ff3cf..b72e8ab 100644
--- a/src/components/Queue/Header.tsx
+++ b/src/components/Queue/Header.tsx
@@ -1,6 +1,7 @@
import { Container, createStyles, Image, Text, Tooltip } from "@mantine/core";
import dayjs from "dayjs";
import getConfig from "next/config";
+import { escapeURL } from "../../util/util";
const useStyles = createStyles((theme) => ({
queueHeader: {
@@ -20,12 +21,10 @@ const useStyles = createStyles((theme) => ({
},
},
queueHeaderTitle: {
- fontFamily: "Inter",
fontWeight: 600,
fontSize: "24px",
},
queueHeaderHoverText: {
- fontFamily: "Inter",
fontWeight: 600,
fontSize: "18px",
},
@@ -33,7 +32,6 @@ const useStyles = createStyles((theme) => ({
paddingLeft: "10px",
},
liveArchive: {
- fontFamily: "Inter",
fontWeight: 600,
fontSize: "18px",
backgroundColor: theme.colors.red[8],
@@ -43,7 +41,6 @@ const useStyles = createStyles((theme) => ({
marginRight: "7px",
},
onHold: {
- fontFamily: "Inter",
fontWeight: 600,
fontSize: "18px",
backgroundColor: theme.colors.indigo[8],
@@ -64,7 +61,8 @@ const QueueHeader = ({ queue }: Object) => {
@@ -101,7 +99,7 @@ const QueueHeader = ({ queue }: Object) => {
title="Streamed At"
className={classes.queueHeaderHoverText}
>
- {dayjs(queue.edges.vod.streamed_at).format("DD.MM.YYYY")}
+ {dayjs(queue.edges.vod.streamed_at).format("YYYY/MM/DD")}
diff --git a/src/components/Queue/Table.tsx b/src/components/Queue/Table.tsx
index ae7bf72..3e920bd 100644
--- a/src/components/Queue/Table.tsx
+++ b/src/components/Queue/Table.tsx
@@ -107,7 +107,7 @@ const QueueTable = () => {
return false;
};
- if (error) return
Fehler beim Laden
;
+ if (error) return
Failed to load
;
if (isLoading) return
;
return (
@@ -139,14 +139,14 @@ const QueueTable = () => {
)}
{value.processing && !checkFailed(value) && !value.on_hold && (
-
+
)}
{value.processing && !checkFailed(value) && value.on_hold && (
-
+
@@ -159,21 +159,21 @@ const QueueTable = () => {
{
accessor: "live_archive",
- title: "Live-Archiv",
+ title: "Live Archive",
render: ({ live_archive }) => (
{live_archive ? "true" : "false"}
),
},
{
accessor: "created_at",
- title: "Erstellt am",
+ title: "Created At",
render: ({ created_at }) => (
{new Date(created_at).toLocaleString()}
),
},
{
accessor: "actions",
- title: "Aktionen",
+ title: "Actions",
render: ({ id, live_archive }) => (
{
>
logs
-
{" - "}
-
restartTask("video_download")}
- >
- restart
-
+ {!queue.live_archive && (
+
{" - "}
+ restartTask("video_download")}
+ >
+ restart
+
+ )}
+
diff --git a/src/components/Vod/Card.tsx b/src/components/Vod/Card.tsx
index 0120a19..3b92592 100644
--- a/src/components/Vod/Card.tsx
+++ b/src/components/Vod/Card.tsx
@@ -27,6 +27,7 @@ import { IconCircleCheck, IconMenu2 } from "@tabler/icons";
import { ROLES } from "../../hooks/useJsxAuth";
import { VodMenu } from "./Menu";
import Link from "next/link";
+import { escapeURL } from "../../util/util";
dayjs.extend(duration);
dayjs.extend(localizedFormat);
@@ -176,14 +177,16 @@ const VideoCard = ({
- Das Bild konnte nicht geladen werden!
+ This image could not be loaded
}
/>
@@ -240,13 +243,13 @@ const VideoCard = ({
- {dayjs(video.streamed_at).format("DD.MM.YYYY")}{" "}
+ {dayjs(video.streamed_at).format("YYYY/MM/DD")}{" "}
{user.settings.moreUIDetails && (
{dayjs(video.streamed_at).format("LT")}
)}
diff --git a/src/components/Vod/ChatPlayer.tsx b/src/components/Vod/ChatPlayer.tsx
index 8fd08e5..9f394e8 100644
--- a/src/components/Vod/ChatPlayer.tsx
+++ b/src/components/Vod/ChatPlayer.tsx
@@ -4,6 +4,7 @@ import { createStyles } from "@mantine/core";
import React, { useEffect, useRef } from "react";
import vodDataBus from "./EventBus";
import getConfig from "next/config";
+import { escapeURL } from "../../util/util";
const useStyles = createStyles((theme) => ({
chatPlayer: {
@@ -75,7 +76,7 @@ export const VodChatPlayer = ({ vod }: any) => {
title: vod.title,
sources: [
{
- src: `${publicRuntimeConfig.CDN_URL}${vod.chat_video_path}`,
+ src: `${publicRuntimeConfig.CDN_URL}${escapeURL(vod.chat_video_path)}`,
type: "video/mp4",
},
],
diff --git a/src/components/Vod/ExperimentalChatPlayer.tsx b/src/components/Vod/ExperimentalChatPlayer.tsx
index fd2fe44..2f34255 100644
--- a/src/components/Vod/ExperimentalChatPlayer.tsx
+++ b/src/components/Vod/ExperimentalChatPlayer.tsx
@@ -65,7 +65,7 @@ const ExperimentalChatPlayer = ({ vod }: any) => {
const data = await axios.get(
`${publicRuntimeConfig.API_URL}/api/v1/vod/${vod.id}/chat/emotes`
);
- console.log(`Es wurden ${data.data.emotes.length} emotes geladen`);
+ console.log(`Loaded ${data.data.emotes.length} emotes`);
data.data.emotes.forEach((emote: GanymedeEmote) => {
if (emote.name == null || emote.name == "") {
// If Twitch emote, see the emote ID as the key
@@ -130,9 +130,9 @@ const ExperimentalChatPlayer = ({ vod }: any) => {
setReady(true);
internalReady = true;
- createCustomComment("Chat-Player bereit.");
+ createCustomComment("Chat player ready.");
createCustomComment(
- `Es wurden ${generalBadgeMap.size.toLocaleString()} badges, ${subscriptionBadgeMap.size.toLocaleString()} subscription badges, und ${emoteMap.size.toLocaleString()} emotes geladen.`
+ `Fetched ${generalBadgeMap.size.toLocaleString()} badges, ${subscriptionBadgeMap.size.toLocaleString()} subscription badges, and ${emoteMap.size.toLocaleString()} emotes.`
);
});
});
@@ -369,7 +369,7 @@ const ExperimentalChatPlayer = ({ vod }: any) => {
_id: randomId(),
content_offset_seconds: 0,
commenter: {
- display_name: "Olaf der Weidengeist",
+ display_name: "Ganymede",
},
message: {
body: message,
diff --git a/src/components/Vod/InfoModalContent.tsx b/src/components/Vod/InfoModalContent.tsx
index d016a6d..31b013b 100644
--- a/src/components/Vod/InfoModalContent.tsx
+++ b/src/components/Vod/InfoModalContent.tsx
@@ -29,7 +29,7 @@ export const VodInfoModalContent = ({ vod }: any) => {
Video FFprobe
- {error && Fehler beim Laden
}
+ {error && failed to load
}
{isLoading && }
{data && {JSON.stringify(data.data, null, 2)}
}
diff --git a/src/components/Vod/LoginRequred.tsx b/src/components/Vod/LoginRequred.tsx
index 1ae845c..b11f880 100644
--- a/src/components/Vod/LoginRequred.tsx
+++ b/src/components/Vod/LoginRequred.tsx
@@ -3,6 +3,7 @@ import { IconLock } from "@tabler/icons";
import getConfig from "next/config";
import React from "react";
import { Video } from "../../ganymede-defs";
+import { escapeURL } from "../../util/util";
const useStyles = createStyles((theme) => ({
container: {
@@ -46,7 +47,7 @@ const VodLoginRequired = (vod: Video) => {
const getImageUrl = () => {
if (vod.thumbnail_path) {
- return `${publicRuntimeConfig.CDN_URL}${vod.thumbnail_path}`;
+ return `${publicRuntimeConfig.CDN_URL}${escapeURL(vod.thumbnail_path)}`;
} else {
return "/images/landing-hero.webp";
}
@@ -63,7 +64,7 @@ const VodLoginRequired = (vod: Video) => {
- Du musst eingeloggt sein, um dieses Video anzuschauen!
+ You must be logged in to view this video
diff --git a/src/components/Vod/Menu.tsx b/src/components/Vod/Menu.tsx
index 18d557b..c08b120 100644
--- a/src/components/Vod/Menu.tsx
+++ b/src/components/Vod/Menu.tsx
@@ -18,6 +18,7 @@ import {
IconTrashX,
IconLock,
IconLockOpen,
+ IconShare,
} from "@tabler/icons";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
@@ -26,6 +27,7 @@ import { VodInfoModalContent } from "./InfoModalContent";
import { VodPlaylistModalContent } from "./PlaylistModalContent";
import { ROLES, useJsxAuth } from "../../hooks/useJsxAuth";
import AdminVodDelete from "../Admin/Vods/Delete";
+import vodDataBus from "./EventBus";
const useStyles = createStyles((theme) => ({
action: {
@@ -74,8 +76,8 @@ export const VodMenu = ({ vod, style }: any) => {
).then(() => {
queryClient.invalidateQueries(["playback-data"]);
showNotification({
- title: "Als gesehen markiert",
- message: "VOD wurde erfolgreich als angesehen markiert",
+ title: "Marked as Watched",
+ message: "VOD has been successfully marked as watched",
});
});
},
@@ -94,8 +96,8 @@ export const VodMenu = ({ vod, style }: any) => {
).then(() => {
queryClient.invalidateQueries(["vod", vod.id]);
showNotification({
- title: "Video aktualisiert",
- message: `Das Video wurde ${lock ? "gesperrt" : "entsperrt"}`,
+ title: "Video Updated",
+ message: `Video has been ${lock ? "locked" : "unlocked"}`,
});
if (lock == true) {
isLocked.current = true;
@@ -120,8 +122,8 @@ export const VodMenu = ({ vod, style }: any) => {
).then(() => {
queryClient.invalidateQueries(["playback-data"]);
showNotification({
- title: "Als ungesehen markiert",
- message: "VOD wurde erfolgreich als ungesehen markiert",
+ title: "Marked as Unwatched",
+ message: "VOD has been successfully marked as unwatched",
});
});
},
@@ -135,6 +137,46 @@ export const VodMenu = ({ vod, style }: any) => {
setDeletedOpened(false);
};
+ const shareVideo = () => {
+ let shareUrl: string = "";
+ const url = window.location.origin;
+
+ // check if we are on a vod page
+ if (window.location.pathname.includes("/vods/") && window.location.pathname.includes(vod.id)) {
+ // get the current time
+ const { time } = vodDataBus.getData();
+ const roundedTime = Math.ceil(time);
+ // create the url
+ shareUrl = `${url}/vods/${vod.id}?t=${roundedTime}`;
+ } else {
+ // create the url
+ shareUrl = `${url}/vods/${vod.id}`;
+ }
+
+ // copy the url to the clipboard
+ try {
+ navigator.clipboard.writeText(shareUrl);
+
+ // show a notification
+ showNotification({
+ title: "Copied to Clipboard",
+ message: "The video url has been copied to your clipboard",
+ });
+
+ } catch (err) {
+ console.error(err);
+ // show a notification
+ showNotification({
+ title: "Error",
+ message: "The clipboard API requires HTTPS, falling back to a prompt",
+ color: "red",
+ });
+
+ // fallback to a prompt
+ prompt("Copy to clipboard: Ctrl+C, Enter", shareUrl);
+ }
+ }
+
return (
@@ -170,43 +212,49 @@ export const VodMenu = ({ vod, style }: any) => {
onClick={() => markAsWatched.mutate()}
icon={ }
>
- Als gesehen markieren
+ Mark as Watched
markAsUnWatched.mutate()}
icon={ }
>
- Als nicht angesehen markieren
+ Mark as Unwatched
{isLocked.current ? (
lockVod.mutate(false)}
icon={ }
>
- Entsperren
+ Unlock
) : (
lockVod.mutate(true)}
icon={ }
>
- Sperren
+ Lock
)}
{useJsxAuth({
loggedIn: true,
roles: [ROLES.ADMIN],
}) && (
- {
- openDeleteModal();
- }}
- icon={ }
- >
- Löschen
-
- )}
+ {
+ openDeleteModal();
+ }}
+ icon={ }
+ >
+ Delete
+
+ )}
+ shareVideo()}
+ icon={ }
+ >
+ Share
+
{
opened={infoModalOpened}
onClose={() => setInfoModalOpended(false)}
size="xl"
- title="VOD Informationen"
+ title="VOD Information"
>
setDeletedOpened(false)}
- title="Video Löschen"
+ title="Delete Video"
>
diff --git a/src/components/Vod/PlaylistModalContent.tsx b/src/components/Vod/PlaylistModalContent.tsx
index 5d0f910..f902792 100644
--- a/src/components/Vod/PlaylistModalContent.tsx
+++ b/src/components/Vod/PlaylistModalContent.tsx
@@ -81,7 +81,7 @@ export const VodPlaylistModalContent = ({ vod }: any) => {
},
});
- if (error) return
Fehler beim Laden
;
+ if (error) return
failed to load
;
if (isLoading)
return (
@@ -108,7 +108,7 @@ export const VodPlaylistModalContent = ({ vod }: any) => {
value={value}
onChange={setValue}
searchable
- placeholder="Eine Wiedergabeliste auswählen"
+ placeholder="Select a Playlist"
dropdownPosition="bottom"
/>
({
playerContainer: {
@@ -47,11 +49,13 @@ const NewVideoPlayer = ({ vod }: any) => {
const player = useRef(null);
const playerRemote = useMediaRemote(player);
- const [videoSource, setVideoSource] = useState("");
+ const [videoSource, setVideoSource] = useState([{ src: "", type: "" }]);
const [videoType, setVideoType] = useState("");
const [videoPoster, setVideoPoster] = useState("");
const [videoTitle, setVideoTitle] = useState("");
+ const searchParams = useSearchParams()
+
// Fetch playback data
const { data } = useQuery({
refetchOnWindowFocus: false,
@@ -90,13 +94,20 @@ const NewVideoPlayer = ({ vod }: any) => {
type = "application/x-mpegURL";
}
- setVideoSource(`${publicRuntimeConfig.CDN_URL}${vod.video_path}`);
+ setVideoSource([
+ {
+ src: `${publicRuntimeConfig.CDN_URL}${escapeURL(vod.video_path)}`,
+ type: type,
+ },
+ ]);
setVideoType(type);
setVideoTitle(vod.title);
// If thumbnail
if (vod.thumbnail_path) {
- setVideoPoster(`${publicRuntimeConfig.CDN_URL}${vod.thumbnail_path}`);
+ setVideoPoster(
+ `${publicRuntimeConfig.CDN_URL}${escapeURL(vod.video_path)}`
+ );
}
// If captions
@@ -120,6 +131,12 @@ const NewVideoPlayer = ({ vod }: any) => {
player.current!.currentTime = data.time;
}
+ // Check if time is set in the url
+ const time = searchParams.get("t");
+ if (time) {
+ player.current!.currentTime = parseInt(time);
+ }
+
const mediaFullscreenButton = document.querySelector("media-menu");
const buttonContainer = document.createElement("div");
diff --git a/src/ganymede-defs.d.ts b/src/ganymede-defs.d.ts
index 013e4ab..af541ad 100644
--- a/src/ganymede-defs.d.ts
+++ b/src/ganymede-defs.d.ts
@@ -162,3 +162,8 @@ export interface PlaybackData {
updated_at: Date;
created_at: Date;
}
+
+export interface ProxyItem {
+ url: string;
+ header: string;
+}
diff --git a/src/pages/admin/settings.tsx b/src/pages/admin/settings.tsx
index da17822..2068000 100644
--- a/src/pages/admin/settings.tsx
+++ b/src/pages/admin/settings.tsx
@@ -7,16 +7,21 @@ import {
Title,
Switch,
TextInput,
+ Group,
+ ActionIcon,
+ MultiSelect,
} from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
import { showNotification } from "@mantine/notifications";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
import AdminNotificationsDrawer from "../../components/Admin/Settings/NotificationsDrawer";
import AdminStorageSettingsDrawer from "../../components/Admin/Settings/StorageSettingsDrawer";
import { Authorization, ROLES } from "../../components/ProtectedRoute";
import GanymedeLoader from "../../components/Utils/GanymedeLoader";
import { useApi } from "../../hooks/useApi";
+import { ProxyItem } from "../../ganymede-defs";
+import { IconPlus, IconTrash } from "@tabler/icons";
const useStyles = createStyles((theme) => ({
header: {
@@ -50,6 +55,15 @@ const useStyles = createStyles((theme) => ({
notificationDrawer: {
overflowY: "scroll",
},
+ proxyList: {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ },
+ proxyInput: {
+ width: "100%",
+ marginRight: "1rem",
+ },
}));
const AdminSettingsPage = () => {
@@ -64,6 +78,10 @@ const AdminSettingsPage = () => {
useState(false);
const [saveAsHls, setSaveAsHls] = useState(false);
const [twitchToken, setTwitchToken] = useState("");
+ const [proxyEnabled, setProxyEnabled] = useState(false);
+ const [proxyList, setProxyList] = useState([]);
+ const [proxyWhitelist, setProxyWhitelist] = useState([]);
+ const [channelData, setChannelData] = useState([]);
useDocumentTitle("Ganymede - Admin - Settings");
@@ -82,10 +100,42 @@ const AdminSettingsPage = () => {
setChatRenderArgs(res?.data.parameters.chat_render);
setSaveAsHls(res?.data.archive.save_as_hls);
setTwitchToken(res?.data.parameters.twitch_token);
+ setProxyEnabled(res?.data.livestream.proxy_enabled);
+ setProxyList(res?.data.livestream.proxies);
+ setProxyWhitelist(res?.data.livestream.proxy_whitelist);
return res?.data;
}),
});
+ // Fetch channels
+ const { data: channels } = useQuery({
+ queryKey: ["admin-channels"],
+ queryFn: () => {
+ return useApi(
+ {
+ method: "GET",
+ url: `/api/v1/channel`,
+ withCredentials: true,
+ },
+ false
+ ).then((res) => {
+ const tmpArr = [];
+ res?.data.forEach((channel) => {
+ tmpArr.push({
+ label: channel.name,
+ value: channel.name,
+ });
+ });
+ setChannelData(tmpArr);
+ return res?.data;
+ });
+ },
+ });
+
+ // useEffect(() => {
+ // // a
+ // }, [channelData]);
+
const saveSettings = useMutation({
mutationKey: ["save-settings"],
mutationFn: () => {
@@ -105,6 +155,11 @@ const AdminSettingsPage = () => {
archive: {
save_as_hls: saveAsHls,
},
+ livestream: {
+ proxy_enabled: proxyEnabled,
+ proxies: proxyList,
+ proxy_whitelist: proxyWhitelist,
+ },
},
withCredentials: true,
},
@@ -221,8 +276,23 @@ const AdminSettingsPage = () => {
onChange={(event) => setTwitchToken(event.currentTarget.value)}
placeholder="abcdefg1234567890"
label="Twitch Token"
- description="Supply your Twitch token for downloading subscriber only videos."
+ description="Supply your Twitch token for downloading ad-free livestreams and subscriber only videos."
+ />
+
+ setPostVideoFFmpegArgs(event.currentTarget.value)
+ }
+ placeholder="-c:v copy -c:a copy"
+ label="Video Convert FFmpeg Args"
+ description="Post video download ffmpeg args."
/>
+
+
+
+ Livestream
+
{
setStreamlinkLiveArgs(event.currentTarget.value)
}
placeholder="--force-progress,--force,--twitch-low-latency,--twitch-disable-hosting"
- label="Streamlink Args"
- description="Streamlink arguments for live streams. Must be comma separated."
+ label="Streamlink Parameters"
+ description="For live streams. Must be comma separated."
/>
-
+ Proxies
+
+
+ Archive livestreams through a proxy to prevent ads. Your Twitch
+ token will not be sent to the proxy.
+
+
- setPostVideoFFmpegArgs(event.currentTarget.value)
+ setProxyEnabled(event.currentTarget.checked)
}
- placeholder="-c:v copy -c:a copy"
- label="Video Convert FFmpeg Args"
- description="Post video download ffmpeg args."
+ />
+ {proxyList.map((proxy, index) => (
+
+
{
+ const newProxyList = [...proxyList];
+ newProxyList[index].url = event.currentTarget.value;
+ setProxyList(newProxyList);
+ }}
+ label="Proxy URL"
+ />
+ {
+ const newProxyList = [...proxyList];
+ newProxyList[index].header = event.currentTarget.value;
+ setProxyList(newProxyList);
+ }}
+ label="Header"
+ />
+ {
+ const newProxyList = [...proxyList];
+ newProxyList.splice(index, 1);
+ setProxyList(newProxyList);
+ }}
+ >
+
+
+
+ ))}
+ {
+ const newProxyList = [...proxyList];
+ newProxyList.push({ url: "", header: "" });
+ setProxyList(newProxyList);
+ }}
+ mt={10}
+ leftIcon={ }
+ color="violet"
+ >
+ Add
+
+ {/* proxy whitelist */}
+
diff --git a/src/pages/archive/index.tsx b/src/pages/archive/index.tsx
index b8db277..24152fe 100644
--- a/src/pages/archive/index.tsx
+++ b/src/pages/archive/index.tsx
@@ -66,14 +66,14 @@ const ArchivePage = () => {
);
const [channelData, setChannelData] = useState([]);
const [channelId, setChannelID] = useState("");
- useDocumentTitle("Archive - VodArchiv.net");
+ useDocumentTitle("Archive - Ganymede");
const qualityOptions = [
{ label: "Best", value: "best" },
{ label: "720p60", value: "720p60" },
- { label: "480p30", value: "480p30" },
- { label: "360p30", value: "360p30" },
- { label: "160p30", value: "160p30" },
+ { label: "480p", value: "480p30" },
+ { label: "360p", value: "360p30" },
+ { label: "160p", value: "160p30" },
];
const archiveVodSubmit = useMutation({
@@ -225,7 +225,7 @@ const ArchivePage = () => {
- Gebe eine Video-ID ein oder wähle einen Kanal, um einen Livestream zu archivieren.
+ Enter a video ID or select a channel to archive a livestream
{
{ value: "clip", label: "Clip" },
];
- useDocumentTitle(`${props.channel.display_name} - VodArchiv.net`);
+ useDocumentTitle(`${props.channel.display_name} - Ganymede`);
const queryClient = useQueryClient();
@@ -96,7 +96,7 @@ const ChannelPage = (props: any) => {
setPage(1);
};
- if (error) return Fehler beim Laden
;
+ if (error) return failed to load
;
return (
diff --git a/src/pages/channels/index.tsx b/src/pages/channels/index.tsx
index b2d6a4c..7225c8b 100644
--- a/src/pages/channels/index.tsx
+++ b/src/pages/channels/index.tsx
@@ -21,7 +21,7 @@ const ChannelsPage = () => {
),
});
- if (error) return
Fehler beim Laden
;
+ if (error) return
failed to load
;
if (isLoading) return
;
return (
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index afafcf9..a73a5cb 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -39,7 +39,7 @@ const useStyles = createStyles((theme) => ({
export default function Home() {
const { classes } = useStyles();
- useDocumentTitle("VodArchiv");
+ useDocumentTitle("Ganymede");
const user = useUserStore((state) => state);
@@ -55,7 +55,7 @@ export default function Home() {
)}
- Kürzlich archiviert
+ Recently Archived
diff --git a/src/pages/login.tsx b/src/pages/login.tsx
index 0f21fff..35eadea 100644
--- a/src/pages/login.tsx
+++ b/src/pages/login.tsx
@@ -3,7 +3,7 @@ import { Container } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
const LoginPage = () => {
- useDocumentTitle("VodArchiv.net - Login");
+ useDocumentTitle("Ganymede - Login");
return (
diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx
index 89573c6..4deab06 100644
--- a/src/pages/profile.tsx
+++ b/src/pages/profile.tsx
@@ -46,7 +46,7 @@ const ProfilePage = () => {
const { classes, theme } = useStyles();
- useDocumentTitle("Profile - VodArchiv.net");
+ useDocumentTitle("Profile - Ganymede");
useEffect(() => {
setUseNewChatPlayer(user.settings.useNewChatPlayer);
diff --git a/src/pages/register.tsx b/src/pages/register.tsx
index e53ae5b..7556618 100644
--- a/src/pages/register.tsx
+++ b/src/pages/register.tsx
@@ -4,7 +4,7 @@ import { RegisterForm } from "../components/Authentication/Register";
import { useDocumentTitle } from "@mantine/hooks";
const RegisterPage = () => {
- useDocumentTitle("VodArchiv.net - Registrierung");
+ useDocumentTitle("Ganymede - Register");
return (
diff --git a/src/pages/search/index.tsx b/src/pages/search/index.tsx
index 4250f61..4649e18 100644
--- a/src/pages/search/index.tsx
+++ b/src/pages/search/index.tsx
@@ -29,7 +29,7 @@ const SearchPage = (props: SearchPageProps) => {
const [limit, setLimit] = useState(24);
const handlers = useRef();
- useDocumentTitle("VodArchiv.net - Suche");
+ useDocumentTitle("Ganymede - Search");
useEffect(() => {
if (props.q && props.q.length > 0) {
diff --git a/src/pages/vods/[vodId].tsx b/src/pages/vods/[vodId].tsx
index 644be52..74c1bf5 100644
--- a/src/pages/vods/[vodId].tsx
+++ b/src/pages/vods/[vodId].tsx
@@ -55,7 +55,7 @@ const VodPage = (props: any) => {
const { ref, toggle, fullscreen } = useFullscreen();
const isSmallDevice = useRef(false);
- useDocumentTitle(`VodArchiv.net - VOD ${props.vodId}`);
+ useDocumentTitle(`Ganymede - VOD ${props.vodId}`);
const { data } = useQuery({
queryKey: ["vod", props.vodId],
@@ -107,7 +107,7 @@ const VodPage = (props: any) => {
return (
-
{data.title} - VodArchiv.net
+ {data.title} - Ganymede
{checkLoginRequired() && }
{!checkLoginRequired() && (
diff --git a/src/util/util.ts b/src/util/util.ts
new file mode 100644
index 0000000..a76351c
--- /dev/null
+++ b/src/util/util.ts
@@ -0,0 +1,3 @@
+export function escapeURL(str: string): string {
+ return str.replace(/#/g, "%23");
+}