diff --git a/frontend/src/components/SeriesModal.jsx b/frontend/src/components/SeriesModal.jsx
index dcfebf86..48677646 100644
--- a/frontend/src/components/SeriesModal.jsx
+++ b/frontend/src/components/SeriesModal.jsx
@@ -17,7 +17,9 @@ import {
Table,
Divider,
} from '@mantine/core';
-import { Play } from 'lucide-react';
+import { Play, Copy } from 'lucide-react';
+import { notifications } from '@mantine/notifications';
+import { copyToClipboard } from '../utils';
import useVODStore from '../store/useVODStore';
import useVideoStore from '../store/useVideoStore';
import useSettingsStore from '../store/settings';
@@ -262,6 +264,39 @@ const SeriesModal = ({ series, opened, onClose }) => {
showVideo(streamUrl, 'vod', episode);
};
+ const getEpisodeStreamUrl = (episode) => {
+ let streamUrl = `/proxy/vod/episode/${episode.uuid}`;
+
+ // Add selected provider as query parameter if available
+ if (selectedProvider) {
+ // Use stream_id for most specific selection, fallback to account_id
+ if (selectedProvider.stream_id) {
+ streamUrl += `?stream_id=${encodeURIComponent(selectedProvider.stream_id)}`;
+ } else {
+ streamUrl += `?m3u_account_id=${selectedProvider.m3u_account.id}`;
+ }
+ }
+
+ if (env_mode === 'dev') {
+ streamUrl = `${window.location.protocol}//${window.location.hostname}:5656${streamUrl}`;
+ } else {
+ streamUrl = `${window.location.origin}${streamUrl}`;
+ }
+ return streamUrl;
+ };
+
+ const handleCopyEpisodeLink = async (episode) => {
+ const streamUrl = getEpisodeStreamUrl(episode);
+ const success = await copyToClipboard(streamUrl);
+ notifications.show({
+ title: success ? 'Link Copied!' : 'Copy Failed',
+ message: success
+ ? 'Episode link copied to clipboard'
+ : 'Failed to copy link to clipboard',
+ color: success ? 'green' : 'red',
+ });
+ };
+
const handleEpisodeRowClick = (episode) => {
setExpandedEpisode(expandedEpisode === episode.id ? null : episode.id);
};
@@ -611,20 +646,34 @@ const SeriesModal = ({ series, opened, onClose }) => {
- 0 && !selectedProvider
- }
- onClick={(e) => {
- e.stopPropagation();
- handlePlayEpisode(episode);
- }}
- >
-
-
+
+ 0 &&
+ !selectedProvider
+ }
+ onClick={(e) => {
+ e.stopPropagation();
+ handlePlayEpisode(episode);
+ }}
+ >
+
+
+ {
+ e.stopPropagation();
+ handleCopyEpisodeLink(episode);
+ }}
+ >
+
+
+
{expandedEpisode === episode.id && (
diff --git a/frontend/src/components/VODModal.jsx b/frontend/src/components/VODModal.jsx
index 90fd3fad..7b1d34eb 100644
--- a/frontend/src/components/VODModal.jsx
+++ b/frontend/src/components/VODModal.jsx
@@ -13,7 +13,9 @@ import {
Stack,
Modal,
} from '@mantine/core';
-import { Play } from 'lucide-react';
+import { Play, Copy } from 'lucide-react';
+import { notifications } from '@mantine/notifications';
+import { copyToClipboard } from '../utils';
import useVODStore from '../store/useVODStore';
import useVideoStore from '../store/useVideoStore';
import useSettingsStore from '../store/settings';
@@ -232,9 +234,9 @@ const VODModal = ({ vod, opened, onClose }) => {
}
}, [opened]);
- const handlePlayVOD = () => {
+ const getStreamUrl = () => {
const vodToPlay = detailedVOD || vod;
- if (!vodToPlay) return;
+ if (!vodToPlay) return null;
let streamUrl = `/proxy/vod/movie/${vod.uuid}`;
@@ -253,9 +255,29 @@ const VODModal = ({ vod, opened, onClose }) => {
} else {
streamUrl = `${window.location.origin}${streamUrl}`;
}
+ return streamUrl;
+ };
+
+ const handlePlayVOD = () => {
+ const streamUrl = getStreamUrl();
+ if (!streamUrl) return;
+ const vodToPlay = detailedVOD || vod;
showVideo(streamUrl, 'vod', vodToPlay);
};
+ const handleCopyLink = async () => {
+ const streamUrl = getStreamUrl();
+ if (!streamUrl) return;
+ const success = await copyToClipboard(streamUrl);
+ notifications.show({
+ title: success ? 'Link Copied!' : 'Copy Failed',
+ message: success
+ ? 'Stream link copied to clipboard'
+ : 'Failed to copy link to clipboard',
+ color: success ? 'green' : 'red',
+ });
+ };
+
// Helper to get embeddable YouTube URL
const getEmbedUrl = (url) => {
if (!url) return '';
@@ -486,6 +508,16 @@ const VODModal = ({ vod, opened, onClose }) => {
Watch Trailer
)}
+ }
+ variant="outline"
+ color="gray"
+ size="sm"
+ onClick={handleCopyLink}
+ style={{ alignSelf: 'flex-start' }}
+ >
+ Copy Link
+