diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f77f5ace..c759990c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,5 +1,8 @@
name: Build
-on: [push, pull_request]
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
jobs:
windows32:
diff --git a/README.md b/README.md
index cfdb76e2..f8741a8c 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@
-->
# Syncplay
-
+
Solution to synchronize video playback across multiple instances of mpv, VLC, MPC-HC, MPC-BE and mplayer2 over the Internet.
@@ -32,6 +32,25 @@ https://syncplay.pl
## Download
https://syncplay.pl/download/
+## This fork (lssb-strm)
+
+This is a personal build of Syncplay 1.7.7 for watching **`.strm` libraries** (Emby / Jellyfin / NAS pointers) together. It does not replace upstream Syncplay; the official project remains at https://github.com/Syncplay/syncplay.
+
+Related report: [Syncplay/syncplay#807](https://github.com/Syncplay/syncplay/issues/807).
+
+### What changed
+
+- **Shared playlist identity.** mpv treats `.strm` as a playlist and reports the inner HTTP URL as the current file. This build keeps the `.strm` filename as the room identity for 25 seconds after open, so two people can each play their own NAS URL while staying on the same playlist item.
+- **Sidecar subtitles.** Syncplay starts mpv with a bundled `strm-sidecars.lua` (`--scripts-append`). Matching `.srt` / `.ass` files next to the `.strm` are loaded automatically. You do not need to change the mpv install.
+- **Server.** No server change. A 1.7.7 server is fine.
+- **About dialog.** Window title is `Syncplay v1.7.7 lssb-strm`. Help → About names **lssb** and **白小九日**.
+
+### 本次修改(中文)
+
+mpv 会把 `.strm` 展开成里面的 URL,Syncplay 会把它当成另一个文件,同名字幕也找不到。本 fork 做了两件事:房间里继续用 `.strm` 文件名(打开后 25 秒内完成绑定),启动 mpv 时带上内置脚本,从 `.strm` 所在目录加载同名字幕。服务端不用改。
+
+Windows 64 位便携包请到本仓库 [Actions](https://github.com/lssb/syncplay/actions) 下载 `Syncplay_1.7.7_x64_Portable`(工作流名:Build,分支 `strm-identity`)。
+
## What does it do
Syncplay synchronises the position and play state of multiple media players so that the viewers can watch the same thing at the same time.
diff --git a/buildPy2exe.py b/buildPy2exe.py
index 048fcd8c..7cbda371 100755
--- a/buildPy2exe.py
+++ b/buildPy2exe.py
@@ -718,6 +718,7 @@ def run(self):
resources = [
"syncplay/resources/syncplayintf.lua",
+ "syncplay/resources/strm-sidecars.lua",
"syncplay/resources/license.rtf",
"syncplay/resources/third-party-notices.txt"
]
diff --git a/syncplay/__init__.py b/syncplay/__init__.py
index 4f27f610..4188e4a4 100755
--- a/syncplay/__init__.py
+++ b/syncplay/__init__.py
@@ -1,5 +1,9 @@
version = '1.7.7'
-revision = ' development'
+revision = ' lssb-strm'
milestone = 'Yoitsu'
release_number = '120'
projectURL = 'https://syncplay.pl/'
+custom_build_note = (
+ "本版本包含 .strm 播放列表身份与同名字幕补丁,由 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 cae8de1c..56673f2e 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 57d5c091..1ef0eb18 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 12ee2c3f..17fd6f91 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 5df859eb..c1fc0b80 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 214a68e8..1b2a8b51 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 00000000..e480509d
--- /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 00000000..97d920d6
--- /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 7bf86f34..9b295d5f 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):
"
" + custom_build_note + "
Copyright © 2012–2026 Syncplay
" + getMessage("about-dialog-license-text") + "