diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 6990d9d95..f29abd59d 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -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 { diff --git a/internal/api/routes_scene.go b/internal/api/routes_scene.go index 2905bd53a..a3fa766aa 100644 --- a/internal/api/routes_scene.go +++ b/internal/api/routes_scene.go @@ -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) } diff --git a/ui/v2.5/graphql/data/config.graphql b/ui/v2.5/graphql/data/config.graphql index 08dcf5d3b..65fc8b1a7 100644 --- a/ui/v2.5/graphql/data/config.graphql +++ b/ui/v2.5/graphql/data/config.graphql @@ -113,6 +113,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult { handyKey funscriptOffset useStashHostedFunscript + showOpenExternal } fragment ConfigDLNAData on ConfigDLNAResult { diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx index 3701f4138..5a7b6f8d7 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx @@ -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 = ({ 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 ; const { stream } = paths; @@ -47,6 +55,8 @@ export const ExternalPlayerButton: React.FC = ({ url = streamURL .toString() .replace(new RegExp(`^${streamURL.protocol}`), "vlc-x-callback:"); + } else if (showOpenExternal) { + url = stream + "/org/" + encodeURIComponent(fileName); } return ( diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 0ebe3f736..cffe1b410 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -462,13 +462,18 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( return {TextUtils.secondsToTimestamp(v ?? 0)}; }} /> - saveUI({ showAbLoopControls: v })} /> + saveUI({ showOpenExternal: v })} + />