This commit is contained in:
Philip Wang 2026-02-06 10:28:25 +02:00 committed by GitHub
commit b21214e1a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 70 additions and 4 deletions

View file

@ -416,6 +416,9 @@ input ConfigInterfaceInput {
noBrowser: Boolean
"True if we should send notifications to the desktop"
notificationsEnabled: Boolean
"True if 'Open in Ext. Player' button in scene details is shown"
showOpenExternal: Boolean
}
type ConfigDisableDropdownCreate {
@ -489,6 +492,9 @@ type ConfigInterfaceResult {
funscriptOffset: Int
"Whether to use Stash Hosted Funscript"
useStashHostedFunscript: Boolean
"Show 'Open in Ext. Player' button in scene details"
showOpenExternal: Boolean
}
input ConfigDLNAInput {

View file

@ -5,6 +5,8 @@ import (
"context"
"errors"
"net/http"
"net/url"
"os"
"strconv"
"strings"
@ -68,6 +70,7 @@ func (rs sceneRoutes) Routes() chi.Router {
r.Get("/stream.mpd", rs.StreamDASH)
r.Get("/stream.mpd/{segment}_v.webm", rs.StreamDASHVideoSegment)
r.Get("/stream.mpd/{segment}_a.webm", rs.StreamDASHAudioSegment)
r.Get("/stream/org/{streamOrgFile}", rs.StreamOrgDirect)
r.Get("/screenshot", rs.Screenshot)
r.Get("/preview", rs.Preview)
@ -99,6 +102,44 @@ func (rs sceneRoutes) StreamDirect(w http.ResponseWriter, r *http.Request) {
ss.StreamSceneDirect(scene, w, r)
}
func (rs sceneRoutes) StreamOrgDirect(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
// check if it's funscript
streamOrgFile := chi.URLParam(r, "streamOrgFile")
aStr := strings.Split(streamOrgFile, ".")
// aStr usually is the primary file, but can be .srt or other file format.
if strings.ToLower(aStr[len(aStr)-1]) == "funscript" {
// it's a funscript request
rs.Funscript(w, r)
return
}
// return 404 if the scene does not have a primary file
primaryFile := scene.Files.Primary()
if primaryFile == nil {
w.WriteHeader(http.StatusNotFound)
if _, err := w.Write([]byte("Primary file not found for streaming original file.")); err != nil {
logger.Warnf("[scene] error getting primary file for streaming original: $v", err)
}
return
}
pathBase := primaryFile.Path[:len(primaryFile.Path)-len(primaryFile.Basename)] // remove filename from the path.
f, err := url.PathUnescape(streamOrgFile)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
f = pathBase + f
// Also return 404 if the actual file cannot be found
if _, err := os.Stat(f); errors.Is(err, os.ErrNotExist) {
w.WriteHeader(http.StatusNotFound)
return
}
utils.ServeStaticFile(w, r, f)
// http.ServeFile(w, r, f.Path)
}
func (rs sceneRoutes) StreamMp4(w http.ResponseWriter, r *http.Request) {
rs.streamTranscode(w, r, ffmpeg.StreamTypeMP4)
}

View file

@ -113,6 +113,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
handyKey
funscriptOffset
useStashHostedFunscript
showOpenExternal
}
fragment ConfigDLNAData on ConfigDLNAResult {

View file

@ -5,6 +5,7 @@ import { useIntl } from "react-intl";
import { Icon } from "src/components/Shared/Icon";
import { objectTitle } from "src/core/files";
import { SceneDataFragment } from "src/core/generated-graphql";
import { useConfigurationContext } from "src/hooks/Config";
export interface IExternalPlayerButtonProps {
scene: SceneDataFragment;
@ -16,10 +17,17 @@ export const ExternalPlayerButton: React.FC<IExternalPlayerButtonProps> = ({
const isAndroid = /(android)/i.test(navigator.userAgent);
const isAppleDevice = /(ipod|iphone|ipad)/i.test(navigator.userAgent);
const intl = useIntl();
const { configuration } = useConfigurationContext();
const showOpenExternal = configuration.ui.showOpenExternal ?? true;
const { paths, files } = scene;
// Get only file name from the full path.
const fileName = files[0].path?.split("/").pop()?.split("\\").pop() ?? "";
const { paths } = scene;
if (!paths || !paths.stream || (!isAndroid && !isAppleDevice))
if (
!paths ||
!paths.stream ||
(!isAndroid && !isAppleDevice && !showOpenExternal)
)
return <span />;
const { stream } = paths;
@ -47,6 +55,8 @@ export const ExternalPlayerButton: React.FC<IExternalPlayerButtonProps> = ({
url = streamURL
.toString()
.replace(new RegExp(`^${streamURL.protocol}`), "vlc-x-callback:");
} else if (showOpenExternal) {
url = stream + "/org/" + encodeURIComponent(fileName);
}
return (

View file

@ -462,13 +462,18 @@ export const SettingsInterfacePanel: React.FC = PatchComponent(
return <span>{TextUtils.secondsToTimestamp(v ?? 0)}</span>;
}}
/>
<BooleanSetting
id="show-ab-loop"
headingID="config.ui.scene_player.options.show_ab_loop_controls"
checked={ui.showAbLoopControls ?? undefined}
onChange={(v) => saveUI({ showAbLoopControls: v })}
/>
<BooleanSetting
id="show-open-external"
headingID="config.ui.scene_player.options.show_open_external"
checked={ui.showOpenExternal ?? true}
onChange={(v) => saveUI({ showOpenExternal: v })}
/>
</SettingSection>
<SettingSection headingID="config.ui.tag_panel.heading">
<BooleanSetting

View file

@ -83,6 +83,8 @@ export interface IUIConfig {
showAbLoopControls?: boolean;
showOpenExternal?: boolean;
// maximum number of items to shown in the dropdown list - defaults to 200
// upper limit of 1000
maxOptionsShown?: number;

View file

@ -798,6 +798,7 @@
"disable_mobile_media_auto_rotate": "Disable auto-rotate of fullscreen media on Mobile",
"enable_chromecast": "Enable Chromecast",
"show_ab_loop_controls": "Show AB Loop plugin controls",
"show_open_external": "Show 'Open In External Player' button",
"show_scrubber": "Show Scrubber",
"show_range_markers": "Show Range Markers",
"track_activity": "Enable Scene Play history",