From f68b8811fcd645450f1754ecf482c4a2c6ce663e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Sep 2026 08:53:00 +0000 Subject: [PATCH 1/8] feat(web-player): copy media links with middle-click Middle-click a channel to copy its live URL, or a program to copy the catch-up URL for that timeslot, then show a toast on success. Co-authored-by: Stackie Jia --- .../references/translation-memory.md | 1 + docs/en/guide/web-player.md | 10 + docs/guide/web-player.md | 10 + .../components/player/channel-list-item.tsx | 23 +- web-ui/src/components/player/channel-list.tsx | 6 + web-ui/src/components/player/epg-view.tsx | 37 ++- web-ui/src/components/player/player-toast.tsx | 56 ++++ web-ui/src/i18n/player.ts | 12 + web-ui/src/lib/catchup-url.test.ts | 43 +++ web-ui/src/lib/catchup-url.ts | 272 +++++++++++++++++ web-ui/src/lib/clipboard.ts | 30 ++ web-ui/src/lib/m3u-parser.test.ts | 32 ++ web-ui/src/lib/m3u-parser.ts | 283 +----------------- web-ui/src/lib/media-direct-link.test.ts | 97 ++++++ web-ui/src/lib/media-direct-link.ts | 102 +++++++ web-ui/src/pages/player.tsx | 45 ++- 16 files changed, 775 insertions(+), 284 deletions(-) create mode 100644 web-ui/src/components/player/player-toast.tsx create mode 100644 web-ui/src/lib/catchup-url.test.ts create mode 100644 web-ui/src/lib/catchup-url.ts create mode 100644 web-ui/src/lib/clipboard.ts create mode 100644 web-ui/src/lib/m3u-parser.test.ts create mode 100644 web-ui/src/lib/media-direct-link.test.ts create mode 100644 web-ui/src/lib/media-direct-link.ts diff --git a/.agents/skills/translate-docs-zh-en/references/translation-memory.md b/.agents/skills/translate-docs-zh-en/references/translation-memory.md index 335d5446..c7e92518 100644 --- a/.agents/skills/translate-docs-zh-en/references/translation-memory.md +++ b/.agents/skills/translate-docs-zh-en/references/translation-memory.md @@ -17,6 +17,7 @@ - 快速换台 -> fast channel change / channel switching - 无缝换台 -> seamless channel switching / Seamless switch (player setting label) - 时移回看 -> time-shifted playback / time-shift / catch-up +- 媒体直链 -> media link / direct media URL - 电子节目单 -> EPG (Electronic Program Guide) - 频道 -> channel - 线路/源 -> source diff --git a/docs/en/guide/web-player.md b/docs/en/guide/web-player.md index 66f1ec65..71cf1ba0 100644 --- a/docs/en/guide/web-player.md +++ b/docs/en/guide/web-player.md @@ -29,9 +29,19 @@ http://server:port/player#CCTV-1 The player automatically updates the address bar when you switch channels, so you can copy the link at any time to share the channel currently playing. +## Copy Media Links + +Middle-click a channel in the channel list to copy that channel's live URL for the current source. Middle-click a program in the program guide to copy that program's catch-up URL (the channel must have `catchup-source` configured). A toast appears after a successful copy. + +The copied value is a directly requestable HTTP media URL, which you can download with FFmpeg, N_m3u8DL-RE, IDM, or open in a third-party player. For multi-source channels, the most recently used source is copied. If the current page includes `r2h-token`, the copied link includes that parameter as well. + +> [!NOTE] +> An on-air program with no catch-up source copies the live URL. A program that has not started and has no catch-up source cannot be copied. + ## Features - **Channel List**: Automatically loads configured M3U channel lists +- **Copy Media Links**: Middle-click a channel or program to copy its live or catch-up URL - **Live Streaming**: Watch live broadcasts directly in the browser - **Time-Shifted Playback**: Supports EPG (Electronic Program Guide) and time-shifted playback (requires catchup source) - **Fast Startup**: Achieves millisecond-level channel switching with FCC diff --git a/docs/guide/web-player.md b/docs/guide/web-player.md index 94f721eb..c2819607 100644 --- a/docs/guide/web-player.md +++ b/docs/guide/web-player.md @@ -29,9 +29,19 @@ http://服务器地址:端口/player#CCTV-1 播放器在切换频道时会自动更新地址栏,随时复制即可分享当前正在播放的频道。 +## 复制媒体直链 + +在频道列表中键点击频道,可复制该频道当前线路的直播地址。在节目单中键点击节目,可复制该节目的回看地址(需要频道配置了 `catchup-source`)。复制成功后页面会弹出提示。 + +复制得到的是可直接请求的 HTTP 媒体地址,可用于 FFmpeg、N_m3u8DL-RE、IDM 等工具下载或在第三方播放器中打开。多线路频道会复制最近一次使用的线路;若当前页面带有 `r2h-token`,复制的链接也会带上该参数。 + +> [!NOTE] +> 正在播出且没有回看源的节目会复制直播地址。尚未开始且没有回看源的节目无法复制。 + ## 功能特性 - **频道列表**:自动加载配置的 M3U 频道列表 +- **复制媒体直链**:中键点击频道或节目,复制直播或回看地址 - **在线直播**:在浏览器中直接观看直播 - **时移回看**:支持 EPG 电子节目单和时移回看(需要有回看源) - **快速起播**:搭配 FCC 可实现毫秒级换台速度 diff --git a/web-ui/src/components/player/channel-list-item.tsx b/web-ui/src/components/player/channel-list-item.tsx index 804d4878..01a5a7ca 100644 --- a/web-ui/src/components/player/channel-list-item.tsx +++ b/web-ui/src/components/player/channel-list-item.tsx @@ -1,8 +1,9 @@ import { clsx } from "clsx"; import { History } from "lucide-react"; -import { forwardRef, memo, useCallback } from "react"; +import { forwardRef, memo, type MouseEvent as ReactMouseEvent, useCallback } from "react"; import { usePlayerTranslation } from "../../hooks/use-player-translation"; import type { Locale } from "../../lib/locale"; +import { isMiddleMouseButton } from "../../lib/media-direct-link"; import type { Channel } from "../../types/player"; import { PLAYER_CHANNEL_LIST_ITEM_CLASS, @@ -17,12 +18,13 @@ interface ChannelListItemProps { channel: Channel; isCurrentChannel: boolean; handleChannelClick: (channel: Channel) => void; + onCopyMediaLink: (channel: Channel) => void; locale: Locale; currentProgram?: string; } const ChannelListItemComponent = forwardRef( - ({ channel, isCurrentChannel, handleChannelClick, locale, currentProgram }, ref) => { + ({ channel, isCurrentChannel, handleChannelClick, onCopyMediaLink, locale, currentProgram }, ref) => { const t = usePlayerTranslation(locale); const groupLabel = channel.groups.join(" / "); @@ -30,11 +32,26 @@ const ChannelListItemComponent = forwardRef) => { + if (isMiddleMouseButton(event)) event.preventDefault(); + }, []); + + const handleAuxClick = useCallback( + (event: ReactMouseEvent) => { + if (!isMiddleMouseButton(event)) return; + event.preventDefault(); + event.stopPropagation(); + onCopyMediaLink(channel); + }, + [channel, onCopyMediaLink], + ); + return (