mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-31 09:38:52 +00:00
Merge branch 'p-stream:production' into production
This commit is contained in:
commit
d4f705c599
9 changed files with 101 additions and 265 deletions
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
[](https://docs.pstream.mov)
|
||||
|
||||
**I _do not_ endorse piracy of any kind I simply enjoy programming and large user counts.**
|
||||
|
||||
## Quick Deploy
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fp-stream%2Fp-stream)
|
||||
|
|
@ -27,12 +25,7 @@
|
|||
|
||||
## Referrers
|
||||
|
||||
- [FMHY (Voted as #1 multi-server streaming site of 2024)](https://fmhy.net)
|
||||
- [Piracy Subreddit Megathread](https://www.reddit.com/r/Piracy/s/iymSloEpXn)
|
||||
- [Toon's Instances](https://erynith.github.io/movie-web-instances)
|
||||
- [Entertainment Empire](https://discord.gg/8NSDNEMfja)
|
||||
- Search Engines: DuckDuckGo, Bing, Google
|
||||
- Rentry.co
|
||||
- [FMHY (Voted as #1 streaming site of 2024, 2025)](https://fmhy.net)
|
||||
|
||||
## Running Locally
|
||||
|
||||
|
|
|
|||
|
|
@ -777,8 +777,8 @@
|
|||
"refreshing": "Refreshing...",
|
||||
"empty": "There are no provided subtitles for this.",
|
||||
"notFound": "None of the available options match your query",
|
||||
"useNativeSubtitles": "Use native video subtitles",
|
||||
"useNativeSubtitlesDescription": "May fix subtitles when casting and in PiP",
|
||||
"useNativeSubtitles": "Native video subtitles",
|
||||
"useNativeSubtitlesDescription": "Broadcast subtitles for native fullscreen and PiP",
|
||||
"delayLate": "Heard audio",
|
||||
"delayEarly": "Saw caption"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,39 +45,6 @@ export function EpisodeCarousel({
|
|||
const updateItem = useProgressStore((s) => s.updateItem);
|
||||
const confirmModal = useModal("season-watch-confirm");
|
||||
|
||||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||
const [canScrollRight, setCanScrollRight] = useState(false);
|
||||
|
||||
const updateScrollState = () => {
|
||||
if (!carouselRef.current) {
|
||||
setCanScrollLeft(false);
|
||||
setCanScrollRight(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollLeft, scrollWidth, clientWidth } = carouselRef.current;
|
||||
const isAtStart = scrollLeft <= 1;
|
||||
const isAtEnd = scrollLeft + clientWidth >= scrollWidth - 1;
|
||||
|
||||
setCanScrollLeft(!isAtStart);
|
||||
setCanScrollRight(!isAtEnd);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const carousel = carouselRef.current;
|
||||
if (!carousel) return;
|
||||
|
||||
updateScrollState();
|
||||
|
||||
carousel.addEventListener("scroll", updateScrollState);
|
||||
window.addEventListener("resize", updateScrollState);
|
||||
|
||||
return () => {
|
||||
carousel.removeEventListener("scroll", updateScrollState);
|
||||
window.removeEventListener("resize", updateScrollState);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleScroll = (direction: "left" | "right") => {
|
||||
if (!carouselRef.current) return;
|
||||
|
||||
|
|
@ -563,17 +530,15 @@ export function EpisodeCarousel({
|
|||
{/* Episodes Carousel */}
|
||||
<div className="relative">
|
||||
{/* Left scroll button */}
|
||||
{canScrollLeft && (
|
||||
<div className="absolute left-0 top-1/2 transform -translate-y-1/2 z-10 px-4 hidden lg:block">
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm"
|
||||
onClick={() => handleScroll("left")}
|
||||
>
|
||||
<Icon icon={Icons.CHEVRON_LEFT} className="text-white/80" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute left-0 top-1/2 transform -translate-y-1/2 z-10 px-4 hidden lg:block">
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm"
|
||||
onClick={() => handleScroll("left")}
|
||||
>
|
||||
<Icon icon={Icons.CHEVRON_LEFT} className="text-white/80" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={carouselRef}
|
||||
|
|
@ -818,17 +783,15 @@ export function EpisodeCarousel({
|
|||
</div>
|
||||
|
||||
{/* Right scroll button */}
|
||||
{canScrollRight && (
|
||||
<div className="absolute right-0 top-1/2 transform -translate-y-1/2 z-10 px-4 hidden lg:block">
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm"
|
||||
onClick={() => handleScroll("right")}
|
||||
>
|
||||
<Icon icon={Icons.CHEVRON_RIGHT} className="text-white/80" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute right-0 top-1/2 transform -translate-y-1/2 z-10 px-4 hidden lg:block">
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm"
|
||||
onClick={() => handleScroll("right")}
|
||||
>
|
||||
<Icon icon={Icons.CHEVRON_RIGHT} className="text-white/80" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -766,39 +766,6 @@ export function EpisodesView({
|
|||
],
|
||||
);
|
||||
|
||||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||
const [canScrollRight, setCanScrollRight] = useState(false);
|
||||
|
||||
const updateScrollState = () => {
|
||||
if (!carouselRef.current) {
|
||||
setCanScrollLeft(false);
|
||||
setCanScrollRight(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollLeft, scrollWidth, clientWidth } = carouselRef.current;
|
||||
const isAtStart = scrollLeft <= 1;
|
||||
const isAtEnd = scrollLeft + clientWidth >= scrollWidth - 1;
|
||||
|
||||
setCanScrollLeft(!isAtStart);
|
||||
setCanScrollRight(!isAtEnd);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const carousel = carouselRef.current;
|
||||
if (!carousel) return;
|
||||
|
||||
updateScrollState();
|
||||
|
||||
carousel.addEventListener("scroll", updateScrollState);
|
||||
window.addEventListener("resize", updateScrollState);
|
||||
|
||||
return () => {
|
||||
carousel.removeEventListener("scroll", updateScrollState);
|
||||
window.removeEventListener("resize", updateScrollState);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleScroll = (direction: "left" | "right") => {
|
||||
if (!carouselRef.current) return;
|
||||
|
||||
|
|
@ -949,22 +916,20 @@ export function EpisodesView({
|
|||
content = (
|
||||
<div className="relative">
|
||||
{/* Horizontal scroll buttons */}
|
||||
{canScrollLeft && (
|
||||
<div
|
||||
className={classNames(
|
||||
"absolute left-0 top-1/2 transform -translate-y-1/2 z-10 px-4",
|
||||
forceCompactEpisodeView ? "hidden" : "hidden lg:block",
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
"absolute left-0 top-1/2 transform -translate-y-1/2 z-10 px-4",
|
||||
forceCompactEpisodeView ? "hidden" : "hidden lg:block",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm"
|
||||
onClick={() => handleScroll("left")}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm"
|
||||
onClick={() => handleScroll("left")}
|
||||
>
|
||||
<Icon icon={Icons.CHEVRON_LEFT} className="text-white/80" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<Icon icon={Icons.CHEVRON_LEFT} className="text-white/80" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={carouselRef}
|
||||
|
|
@ -1031,22 +996,20 @@ export function EpisodesView({
|
|||
</div>
|
||||
|
||||
{/* Right scroll button */}
|
||||
{canScrollRight && (
|
||||
<div
|
||||
className={classNames(
|
||||
"absolute right-0 top-1/2 transform -translate-y-1/2 z-10 px-4",
|
||||
forceCompactEpisodeView ? "hidden" : "hidden lg:block",
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
"absolute right-0 top-1/2 transform -translate-y-1/2 z-10 px-4",
|
||||
forceCompactEpisodeView ? "hidden" : "hidden lg:block",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm"
|
||||
onClick={() => handleScroll("right")}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm"
|
||||
onClick={() => handleScroll("right")}
|
||||
>
|
||||
<Icon icon={Icons.CHEVRON_RIGHT} className="text-white/80" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<Icon icon={Icons.CHEVRON_RIGHT} className="text-white/80" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from "@/components/player/utils/captions";
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
import { SubtitleStyling, useSubtitleStore } from "@/stores/subtitles";
|
||||
|
||||
const wordOverrides: Record<string, string> = {
|
||||
|
|
@ -151,12 +152,17 @@ export function SubtitleRenderer() {
|
|||
|
||||
export function SubtitleView(props: { controlsShown: boolean }) {
|
||||
const caption = usePlayerStore((s) => s.caption.selected);
|
||||
const captionAsTrack = usePlayerStore((s) => s.caption.asTrack);
|
||||
const source = usePlayerStore((s) => s.source);
|
||||
const display = usePlayerStore((s) => s.display);
|
||||
const isCasting = display?.getType() === "casting";
|
||||
const styling = useSubtitleStore((s) => s.styling);
|
||||
const enableNativeSubtitles = usePreferencesStore(
|
||||
(s) => s.enableNativeSubtitles,
|
||||
);
|
||||
|
||||
if (captionAsTrack || !caption || isCasting) return null;
|
||||
// Hide custom captions when native subtitles are enabled
|
||||
const shouldUseNativeTrack = enableNativeSubtitles && source !== null;
|
||||
if (shouldUseNativeTrack || !caption || isCasting) return null;
|
||||
|
||||
return (
|
||||
<Transition animation="slide-up" show>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { makeVideoElementDisplayInterface } from "@/components/player/display/ba
|
|||
import { convertSubtitlesToObjectUrl } from "@/components/player/utils/captions";
|
||||
import { playerStatus } from "@/stores/player/slices/source";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
|
||||
import { useInitializeSource } from "../hooks/useInitializePlayer";
|
||||
|
||||
|
|
@ -66,13 +67,19 @@ function VideoElement() {
|
|||
const trackEl = useRef<HTMLTrackElement>(null);
|
||||
const display = usePlayerStore((s) => s.display);
|
||||
const srtData = usePlayerStore((s) => s.caption.selected?.srtData);
|
||||
const captionAsTrack = usePlayerStore((s) => s.caption.asTrack);
|
||||
const language = usePlayerStore((s) => s.caption.selected?.language);
|
||||
const source = usePlayerStore((s) => s.source);
|
||||
const enableNativeSubtitles = usePreferencesStore(
|
||||
(s) => s.enableNativeSubtitles,
|
||||
);
|
||||
const trackObjectUrl = useObjectUrl(
|
||||
() => (srtData ? convertSubtitlesToObjectUrl(srtData) : null),
|
||||
[srtData],
|
||||
);
|
||||
|
||||
// Use native tracks when the setting is enabled
|
||||
const shouldUseNativeTrack = enableNativeSubtitles && source !== null;
|
||||
|
||||
// report video element to display interface
|
||||
useEffect(() => {
|
||||
if (display && videoEl.current) {
|
||||
|
|
@ -80,17 +87,20 @@ function VideoElement() {
|
|||
}
|
||||
}, [display, videoEl]);
|
||||
|
||||
// select track as showing if it exists
|
||||
// Control track visibility based on setting
|
||||
useEffect(() => {
|
||||
if (trackEl.current) {
|
||||
trackEl.current.track.mode = "showing";
|
||||
trackEl.current.track.mode = shouldUseNativeTrack ? "showing" : "hidden";
|
||||
}
|
||||
}, [trackEl]);
|
||||
}, [shouldUseNativeTrack, trackEl]);
|
||||
|
||||
// Attach track when native subtitles are enabled
|
||||
// SubtitleView handles showing custom captions when native subtitles are disabled
|
||||
let subtitleTrack: ReactNode = null;
|
||||
if (captionAsTrack && trackObjectUrl && language)
|
||||
if (shouldUseNativeTrack && trackObjectUrl && language) {
|
||||
subtitleTrack = (
|
||||
<track
|
||||
ref={trackEl}
|
||||
label="P-Stream Captions"
|
||||
kind="subtitles"
|
||||
srcLang={language}
|
||||
|
|
@ -98,6 +108,7 @@ function VideoElement() {
|
|||
default
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<video
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Flare } from "@/components/utils/Flare";
|
||||
|
||||
|
|
@ -13,12 +11,9 @@ interface CarouselNavButtonsProps {
|
|||
interface NavButtonProps {
|
||||
direction: "left" | "right";
|
||||
onClick: () => void;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
function NavButton({ direction, onClick, visible }: NavButtonProps) {
|
||||
if (!visible) return null;
|
||||
|
||||
function NavButton({ direction, onClick }: NavButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -48,40 +43,6 @@ export function CarouselNavButtons({
|
|||
categorySlug,
|
||||
carouselRefs,
|
||||
}: CarouselNavButtonsProps) {
|
||||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||
const [canScrollRight, setCanScrollRight] = useState(false);
|
||||
|
||||
const updateScrollState = useCallback(() => {
|
||||
const carousel = carouselRefs.current[categorySlug];
|
||||
if (!carousel) {
|
||||
setCanScrollLeft(false);
|
||||
setCanScrollRight(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollLeft, scrollWidth, clientWidth } = carousel;
|
||||
const isAtStart = scrollLeft <= 1;
|
||||
const isAtEnd = scrollLeft + clientWidth >= scrollWidth - 1;
|
||||
|
||||
setCanScrollLeft(!isAtStart);
|
||||
setCanScrollRight(!isAtEnd);
|
||||
}, [categorySlug, carouselRefs]);
|
||||
|
||||
useEffect(() => {
|
||||
const carousel = carouselRefs.current[categorySlug];
|
||||
if (!carousel) return;
|
||||
|
||||
updateScrollState();
|
||||
|
||||
carousel.addEventListener("scroll", updateScrollState);
|
||||
window.addEventListener("resize", updateScrollState);
|
||||
|
||||
return () => {
|
||||
carousel.removeEventListener("scroll", updateScrollState);
|
||||
window.removeEventListener("resize", updateScrollState);
|
||||
};
|
||||
}, [categorySlug, carouselRefs, updateScrollState]);
|
||||
|
||||
const handleScroll = (direction: "left" | "right") => {
|
||||
const carousel = carouselRefs.current[categorySlug];
|
||||
if (!carousel) return;
|
||||
|
|
@ -115,16 +76,8 @@ export function CarouselNavButtons({
|
|||
|
||||
return (
|
||||
<>
|
||||
<NavButton
|
||||
direction="left"
|
||||
onClick={() => handleScroll("left")}
|
||||
visible={canScrollLeft}
|
||||
/>
|
||||
<NavButton
|
||||
direction="right"
|
||||
onClick={() => handleScroll("right")}
|
||||
visible={canScrollRight}
|
||||
/>
|
||||
<NavButton direction="left" onClick={() => handleScroll("left")} />
|
||||
<NavButton direction="right" onClick={() => handleScroll("right")} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
|
||||
interface CategoryButtonsProps {
|
||||
|
|
@ -17,84 +15,34 @@ export function CategoryButtons({
|
|||
isMobile,
|
||||
showAlwaysScroll,
|
||||
}: CategoryButtonsProps) {
|
||||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||
const [canScrollRight, setCanScrollRight] = useState(false);
|
||||
|
||||
const updateScrollState = useCallback(() => {
|
||||
const element = document.getElementById(`button-carousel-${categoryType}`);
|
||||
if (!element) {
|
||||
setCanScrollLeft(false);
|
||||
setCanScrollRight(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollLeft, scrollWidth, clientWidth } = element;
|
||||
const isAtStart = scrollLeft <= 1;
|
||||
const isAtEnd = scrollLeft + clientWidth >= scrollWidth - 1;
|
||||
|
||||
setCanScrollLeft(!isAtStart);
|
||||
setCanScrollRight(!isAtEnd);
|
||||
}, [categoryType]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.getElementById(`button-carousel-${categoryType}`);
|
||||
if (!element) return;
|
||||
|
||||
updateScrollState();
|
||||
|
||||
element.addEventListener("scroll", updateScrollState);
|
||||
window.addEventListener("resize", updateScrollState);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener("scroll", updateScrollState);
|
||||
window.removeEventListener("resize", updateScrollState);
|
||||
};
|
||||
}, [categoryType, updateScrollState]);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
updateScrollState();
|
||||
}, 0);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [categories, categoryType, updateScrollState]);
|
||||
|
||||
const renderScrollButton = (direction: "left" | "right") => {
|
||||
const shouldShow = direction === "left" ? canScrollLeft : canScrollRight;
|
||||
|
||||
if (!shouldShow && !showAlwaysScroll && !isMobile) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center rounded-full px-4 text-white py-3"
|
||||
onClick={() => {
|
||||
const element = document.getElementById(
|
||||
`button-carousel-${categoryType}`,
|
||||
);
|
||||
if (element) {
|
||||
element.scrollBy({
|
||||
left: direction === "left" ? -200 : 200,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
direction === "left" ? Icons.CHEVRON_LEFT : Icons.CHEVRON_RIGHT
|
||||
}
|
||||
className="text-2xl rtl:-scale-x-100"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const renderScrollButton = (direction: "left" | "right") => (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center rounded-full px-4 text-white py-3"
|
||||
onClick={() => {
|
||||
const element = document.getElementById(
|
||||
`button-carousel-${categoryType}`,
|
||||
);
|
||||
if (element) {
|
||||
element.scrollBy({
|
||||
left: direction === "left" ? -200 : 200,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon={direction === "left" ? Icons.CHEVRON_LEFT : Icons.CHEVRON_RIGHT}
|
||||
className="text-2xl rtl:-scale-x-100"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex overflow-x-auto">
|
||||
{(showAlwaysScroll || isMobile || canScrollLeft) &&
|
||||
renderScrollButton("left")}
|
||||
{(showAlwaysScroll || isMobile) && renderScrollButton("left")}
|
||||
|
||||
<div
|
||||
id={`button-carousel-${categoryType}`}
|
||||
|
|
@ -114,8 +62,7 @@ export function CategoryButtons({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{(showAlwaysScroll || isMobile || canScrollRight) &&
|
||||
renderScrollButton("right")}
|
||||
{(showAlwaysScroll || isMobile) && renderScrollButton("right")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { useProgressStore } from "@/stores/progress";
|
||||
|
||||
import { createVersionedStore } from "../migrations";
|
||||
import { OldData, migrateV2Videos } from "./migrations/v2";
|
||||
import { migrateV3Videos } from "./migrations/v3";
|
||||
import { migrateV4Videos } from "./migrations/v4";
|
||||
import { WatchedStoreData } from "./types";
|
||||
import { createVersionedStore } from "../migrations";
|
||||
|
||||
export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
|
||||
.setKey("video-progress")
|
||||
|
|
|
|||
Loading…
Reference in a new issue