From 8afe36b89b7c2c823acd30ee240d902203a9ad3b Mon Sep 17 00:00:00 2001
From: lssb
"
+ "This build adds .strm playlist identity and sidecar subtitle loading. Modified by lssb and 白小九日."
+)
diff --git a/syncplay/client.py b/syncplay/client.py
index cae8de1ce..56673f2e7 100755
--- a/syncplay/client.py
+++ b/syncplay/client.py
@@ -38,6 +38,7 @@
pass
from syncplay import utils, constants, version
+from syncplay.strm import StrmIdentityTracker
from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \
PRIVACY_HIDDENFILENAME
from syncplay.messages import getMissingStrings, getMessage, isNoOSDMessage
@@ -101,6 +102,7 @@ def __init__(self, playerClass, ui, config):
self.lastRewindTime = None
self.lastUpdatedFileTime = None
self.lastAdvanceTime = None
+ self.strmIdentity = StrmIdentityTracker()
self.fileOpenBeforeChangingPlaylistIndex = None
self.waitingToLoadNewfile = False
self.waitingToLoadNewfileSince = None
@@ -562,18 +564,20 @@ def eofReportedByPlayer(self):
def updateFile(self, filename, duration, path):
self.lastUpdatedFileTime = time.time()
newPath = ""
- if utils.isURL(path):
- filename = path
if not path:
return
- try:
- size = os.path.getsize(path)
- except:
+ filename, path, strmSize = self.strmIdentity.canonicalize(filename, path)
+ if strmSize is not None:
+ size = strmSize
+ else:
try:
- path = path.decode('utf-8')
size = os.path.getsize(path)
except:
- size = 0
+ try:
+ path = path.decode('utf-8')
+ size = os.path.getsize(path)
+ except:
+ size = 0
if not utils.isURL(path) and os.path.exists(path):
self.fileSwitch.notifyUserIfFileNotInMediaDirectory(filename, path)
filename, size = self.__executePrivacySettings(filename, size)
@@ -656,6 +660,7 @@ def openFile(self, filePath, resetPosition=False, fromUser=False):
self.playlist.loadPlaylistFromFile(filePath, resetPosition)
return
+ self.strmIdentity.begin_open(filePath)
self.playlist.openedFile()
self._player.openFile(filePath, resetPosition)
if resetPosition:
diff --git a/syncplay/constants.py b/syncplay/constants.py
index 57d5c091a..1ef0eb18e 100755
--- a/syncplay/constants.py
+++ b/syncplay/constants.py
@@ -238,6 +238,7 @@ def getValueForOS(constantDict):
MPV_SENDMESSAGE_COOLDOWN_TIME = 0.05
MPV_MAX_NEWFILE_COOLDOWN_TIME = 3
STREAM_ADDITIONAL_IGNORE_TIME = 10
+STRM_RESOLVE_TIMEOUT = 25
MPV_LOCK_WAIT_TIME = 0.05
VLC_OPEN_MAX_WAIT_TIME = 20
VLC_MIN_PORT = 10000
diff --git a/syncplay/players/iina.py b/syncplay/players/iina.py
index 12ee2c3fe..17fd6f910 100644
--- a/syncplay/players/iina.py
+++ b/syncplay/players/iina.py
@@ -78,6 +78,9 @@ def _preparePlayer(self):
for key, value in constants.IINA_PROPERTIES.items():
self._setProperty(key, value)
self._listener.sendLine(["load-script", findResourcePath("syncplayintf.lua")])
+ sidecar = MpvPlayer.strmSidecarScriptPath()
+ if sidecar:
+ self._listener.sendLine(["load-script", sidecar])
super()._preparePlayer()
def _onFileUpdate(self):
diff --git a/syncplay/players/memento.py b/syncplay/players/memento.py
index 5df859eb5..c1fc0b80c 100644
--- a/syncplay/players/memento.py
+++ b/syncplay/players/memento.py
@@ -4,6 +4,7 @@
from syncplay.players.mpv import MpvPlayer
from syncplay.utils import findResourcePath, playerPathExists
+from syncplay.strm import add_strm_sidecar_script
class MementoPlayer(MpvPlayer):
@@ -52,6 +53,7 @@ def getIconPath(path):
def getStartupArgs(userArgs):
args = constants.MPV_ARGS
args["scripts"] = findResourcePath("syncplayintf.lua")
+ add_strm_sidecar_script(args)
if userArgs:
for argToAdd in userArgs:
if argToAdd.startswith("--"):
diff --git a/syncplay/players/mpv.py b/syncplay/players/mpv.py
index 214a68e8a..1b2a8b517 100755
--- a/syncplay/players/mpv.py
+++ b/syncplay/players/mpv.py
@@ -14,6 +14,7 @@
from syncplay.utils import getRuntimeDir, isURL, findResourcePath
from syncplay.utils import isMacOS, isWindows, isASCII
from syncplay.utils import playerPathExists
+from syncplay.strm import add_strm_sidecar_script
from syncplay.vendor.python_mpv_jsonipc.python_mpv_jsonipc import MPV
class MpvPlayer(BasePlayer):
@@ -53,10 +54,18 @@ def run(client, playerPath, filePath, args):
"Please use mpv >=0.28.0.")
return MpvPlayer(client, MpvPlayer.getExpandedPath(playerPath), filePath, args)
+ @staticmethod
+ def strmSidecarScriptPath():
+ sidecar = findResourcePath("strm-sidecars.lua")
+ if os.path.isfile(sidecar):
+ return sidecar
+ return None
+
@staticmethod
def getStartupArgs(userArgs):
args = constants.MPV_ARGS
args["script"] = findResourcePath("syncplayintf.lua")
+ add_strm_sidecar_script(args)
if userArgs:
for argToAdd in userArgs:
if argToAdd.startswith('--'):
diff --git a/syncplay/resources/strm-sidecars.lua b/syncplay/resources/strm-sidecars.lua
new file mode 100644
index 000000000..e480509dc
--- /dev/null
+++ b/syncplay/resources/strm-sidecars.lua
@@ -0,0 +1,98 @@
+-- Load sidecar subtitles from the directory of a .strm playlist file.
+-- mpv treats .strm as a playlist, so path/filename become the inner URL and
+-- default sub-auto looks next to that URL instead of next to the .strm.
+-- Syncplay starts mpv with --scripts-append pointing at this file.
+
+local utils = require("mp.utils")
+local msg = require("mp.msg")
+
+local SUB_EXTS = {
+ ass = true, srt = true, ssa = true, sub = true, vtt = true,
+ idx = true, lrc = true, smi = true, sup = true,
+}
+
+local last_playlist_path = nil
+
+local function basename(path)
+ if not path then
+ return nil
+ end
+ return path:match("[^/\\]+$")
+end
+
+local function dirname(path)
+ if not path then
+ return nil
+ end
+ return path:match("^(.*)[/\\]")
+end
+
+local function stem(name)
+ if not name then
+ return nil
+ end
+ return name:gsub("%.[^/.]+$", "")
+end
+
+local function extension(name)
+ if not name then
+ return nil
+ end
+ return name:lower():match("%.([^.]+)$")
+end
+
+local function is_matching_sidecar(strm_stem, filename)
+ local ext = extension(filename)
+ if not ext or not SUB_EXTS[ext] then
+ return false
+ end
+ local file_stem = stem(filename)
+ if file_stem == strm_stem then
+ return true
+ end
+ local prefix = strm_stem .. "."
+ return file_stem:sub(1, #prefix) == prefix
+end
+
+local function load_sidecars(playlist_path)
+ if not playlist_path or playlist_path == last_playlist_path then
+ return
+ end
+ if not playlist_path:lower():match("%.strm$") then
+ last_playlist_path = playlist_path
+ return
+ end
+ last_playlist_path = playlist_path
+
+ local dir = dirname(playlist_path)
+ local strm_stem = stem(basename(playlist_path))
+ if not dir or not strm_stem then
+ return
+ end
+
+ local files = utils.readdir(dir, "files")
+ if not files then
+ msg.warn("strm-sidecars: cannot read " .. dir)
+ return
+ end
+
+ local loaded = 0
+ for _, filename in ipairs(files) do
+ if is_matching_sidecar(strm_stem, filename) then
+ local full = utils.join_path(dir, filename)
+ mp.commandv("sub-add", full, "auto")
+ loaded = loaded + 1
+ msg.info("strm-sidecars: added " .. full)
+ end
+ end
+ if loaded == 0 then
+ msg.verbose("strm-sidecars: no sidecars next to " .. playlist_path)
+ end
+end
+
+mp.observe_property("playlist-path", "string", function(_, value)
+ load_sidecars(value)
+end)
+mp.register_event("start-file", function()
+ load_sidecars(mp.get_property("playlist-path"))
+end)
diff --git a/syncplay/strm.py b/syncplay/strm.py
new file mode 100644
index 000000000..97d920d65
--- /dev/null
+++ b/syncplay/strm.py
@@ -0,0 +1,89 @@
+import os
+from urllib.parse import urlparse
+
+from syncplay import constants
+from syncplay import utils
+
+
+def is_strm_path(file_path):
+ if not file_path:
+ return False
+ path = str(file_path)
+ if "://" in path:
+ path = urlparse(path).path
+ return os.path.splitext(path)[1].lower() == ".strm"
+
+
+def _canonical_filename(path):
+ if utils.isURL(path):
+ return path
+ return os.path.basename(path)
+
+
+def add_strm_sidecar_script(args):
+ from syncplay.utils import findResourcePath
+ sidecar = findResourcePath("strm-sidecars.lua")
+ if os.path.isfile(sidecar):
+ args["scripts_append"] = sidecar
+ return args
+
+
+class StrmIdentityTracker:
+ def __init__(self, timeout=None, time_fn=None):
+ self.timeout = constants.STRM_RESOLVE_TIMEOUT if timeout is None else timeout
+ self._time = time_fn
+ self.source = None
+ self.resolved_path = None
+ self.source_time = None
+
+ def _now(self):
+ if self._time is None:
+ import time
+ return time.time()
+ return self._time()
+
+ def clear(self):
+ self.source = None
+ self.resolved_path = None
+ self.source_time = None
+
+ def begin_open(self, file_path):
+ self.clear()
+ if is_strm_path(file_path):
+ self.source = file_path
+ self.source_time = self._now()
+
+ def _resolution_timed_out(self):
+ if self.source_time is None:
+ return True
+ return self._now() - self.source_time > self.timeout
+
+ def canonicalize(self, filename, path):
+ if not path:
+ return filename, path, None
+
+ if is_strm_path(path):
+ self.source = path
+ self.resolved_path = None
+ self.source_time = self._now()
+ return _canonical_filename(path), path, 0
+
+ if self.source:
+ if self.resolved_path is None:
+ if self._resolution_timed_out():
+ self.clear()
+ elif utils.isURL(path):
+ self.resolved_path = path
+ path = self.source
+ return _canonical_filename(path), path, 0
+ else:
+ self.clear()
+ elif path == self.resolved_path:
+ path = self.source
+ return _canonical_filename(path), path, 0
+ else:
+ self.clear()
+
+ if utils.isURL(path):
+ filename = path
+ return filename, path, None
diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py
index 7bf86f342..9b295d5f3 100755
--- a/syncplay/ui/gui.py
+++ b/syncplay/ui/gui.py
@@ -12,7 +12,7 @@
from twisted.internet import task
-from syncplay import utils, constants, version, revision, release_number
+from syncplay import utils, constants, version, revision, release_number, custom_build_note
from syncplay.messages import getMessage
from syncplay.ui.consoleUI import ConsoleUI
from syncplay.utils import resourcespath
@@ -163,6 +163,11 @@ def __init__(self, parent=None):
"
Python " + python_version() + " - " + __binding__ + " " + __binding_version__ +
" - Qt " + __qt_version__ + "
" + custom_build_note + "
Copyright © 2012–2026 Syncplay
" + getMessage("about-dialog-license-text") + "