mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-11 07:46:37 +08:00
feat(strm): auto-append remote directory tail to local output path (#16)
When adding or editing a cloud sync path, append the last segment of the remote directory to the local output directory (e.g. /home/ubuntu/strm + 电影). Remote changes replace the previous auto-appended tail; local browse and blur commit the base path with the current remote tail. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: truewhile <truewhile@users.noreply.github.com>
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
|||||||
normalizeEmbyRemoteLines,
|
normalizeEmbyRemoteLines,
|
||||||
type EmbyRemoteLine,
|
type EmbyRemoteLine,
|
||||||
} from '../utils/embyRemoteLines'
|
} from '../utils/embyRemoteLines'
|
||||||
|
import { lastPathSegment, syncLocalPathWithRemote } from '../utils/strmPaths'
|
||||||
|
|
||||||
// ─── 弹框外壳 ────────────────────────────────────────────────────────────────
|
// ─── 弹框外壳 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -913,10 +914,27 @@ export function StrmSyncPathDialog({
|
|||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [browsing, setBrowsing] = useState(false)
|
const [browsing, setBrowsing] = useState(false)
|
||||||
const [browsingLocal, setBrowsingLocal] = useState<null | 'remote_path' | 'local_path'>(null)
|
const [browsingLocal, setBrowsingLocal] = useState<null | 'remote_path' | 'local_path'>(null)
|
||||||
|
const prevRemoteTailRef = useRef(existing ? lastPathSegment(existing.remote_path) : '')
|
||||||
|
|
||||||
const set = <K extends keyof StrmSyncPathInput>(key: K, value: StrmSyncPathInput[K]) =>
|
const set = <K extends keyof StrmSyncPathInput>(key: K, value: StrmSyncPathInput[K]) =>
|
||||||
setForm((f) => ({ ...f, [key]: value }))
|
setForm((f) => ({ ...f, [key]: value }))
|
||||||
|
|
||||||
|
const updateRemotePath = (remotePath: string) => {
|
||||||
|
setForm((f) => {
|
||||||
|
const synced = syncLocalPathWithRemote(f.local_path, remotePath, prevRemoteTailRef.current)
|
||||||
|
prevRemoteTailRef.current = synced.remoteTail
|
||||||
|
return { ...f, remote_path: remotePath, local_path: synced.localPath }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitLocalPath = (localPath: string) => {
|
||||||
|
setForm((f) => {
|
||||||
|
const synced = syncLocalPathWithRemote(localPath, f.remote_path, prevRemoteTailRef.current)
|
||||||
|
prevRemoteTailRef.current = synced.remoteTail
|
||||||
|
return { ...f, local_path: synced.localPath }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const isLocal = form.provider === 'local'
|
const isLocal = form.provider === 'local'
|
||||||
const availableAccounts = accounts.filter((a) => a.provider === form.provider && a.has_credential)
|
const availableAccounts = accounts.filter((a) => a.provider === form.provider && a.has_credential)
|
||||||
|
|
||||||
@@ -1000,7 +1018,15 @@ export function StrmSyncPathDialog({
|
|||||||
: '远端路径(可通过浏览选择)'
|
: '远端路径(可通过浏览选择)'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<input className={inputCls} value={form.remote_path} placeholder={isLocal ? 'D:\\movies' : '/'} onChange={(e) => set('remote_path', e.target.value)} />
|
<input
|
||||||
|
className={inputCls}
|
||||||
|
value={form.remote_path}
|
||||||
|
placeholder={isLocal ? 'D:\\movies' : '/'}
|
||||||
|
onChange={(e) => set('remote_path', e.target.value)}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (!isLocal) updateRemotePath(e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
{isLocal && (
|
{isLocal && (
|
||||||
@@ -1023,8 +1049,23 @@ export function StrmSyncPathDialog({
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-end gap-2">
|
<div className="flex items-end gap-2">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Field label="本地输出目录" hint="生成的 .strm 与下载的元数据写到这里的对应目录结构下">
|
<Field
|
||||||
<input className={inputCls} value={form.local_path} placeholder="D:\\strm\\movies" onChange={(e) => set('local_path', e.target.value)} />
|
label="本地输出目录"
|
||||||
|
hint={
|
||||||
|
!isLocal && lastPathSegment(form.remote_path)
|
||||||
|
? `将自动拼接远端末级目录「${lastPathSegment(form.remote_path)}」`
|
||||||
|
: '生成的 .strm 与下载的元数据写到这里的对应目录结构下'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className={inputCls}
|
||||||
|
value={form.local_path}
|
||||||
|
placeholder="D:\\strm\\movies"
|
||||||
|
onChange={(e) => set('local_path', e.target.value)}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (!isLocal) commitLocalPath(e.target.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -1109,7 +1150,7 @@ export function StrmSyncPathDialog({
|
|||||||
accountId={form.account_id}
|
accountId={form.account_id}
|
||||||
initialDir={form.remote_path || undefined}
|
initialDir={form.remote_path || undefined}
|
||||||
onSelect={(id) => {
|
onSelect={(id) => {
|
||||||
set('remote_path', id)
|
updateRemotePath(id)
|
||||||
setBrowsing(false)
|
setBrowsing(false)
|
||||||
}}
|
}}
|
||||||
onClose={() => setBrowsing(false)}
|
onClose={() => setBrowsing(false)}
|
||||||
@@ -1120,7 +1161,11 @@ export function StrmSyncPathDialog({
|
|||||||
<LocalDirBrowserDialog
|
<LocalDirBrowserDialog
|
||||||
initialDir={form[browsingLocal] || undefined}
|
initialDir={form[browsingLocal] || undefined}
|
||||||
onSelect={(path) => {
|
onSelect={(path) => {
|
||||||
set(browsingLocal, path)
|
if (browsingLocal === 'local_path') {
|
||||||
|
commitLocalPath(path)
|
||||||
|
} else {
|
||||||
|
set(browsingLocal, path)
|
||||||
|
}
|
||||||
setBrowsingLocal(null)
|
setBrowsingLocal(null)
|
||||||
}}
|
}}
|
||||||
onClose={() => setBrowsingLocal(null)}
|
onClose={() => setBrowsingLocal(null)}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/** 取路径最后一级目录/文件名(兼容 / 与 \\)。 */
|
||||||
|
export function lastPathSegment(path: string): string {
|
||||||
|
const trimmed = path.trim().replace(/[\\/]+$/, '')
|
||||||
|
if (!trimmed) return ''
|
||||||
|
const parts = trimmed.split(/[/\\]/).filter(Boolean)
|
||||||
|
return parts[parts.length - 1] ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathSeparator(path: string): '/' | '\\' {
|
||||||
|
return path.includes('\\') ? '\\' : '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSlashes(path: string): string {
|
||||||
|
return path.replace(/\\/g, '/').replace(/\/+$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 去掉 local 末尾与 prevRemoteTail 匹配的自动拼接段。 */
|
||||||
|
function stripRemoteTail(localPath: string, prevRemoteTail: string): string {
|
||||||
|
if (!prevRemoteTail) return localPath.trim()
|
||||||
|
const trimmed = localPath.trim()
|
||||||
|
if (!trimmed) return ''
|
||||||
|
const norm = normalizeSlashes(trimmed)
|
||||||
|
const suffix = '/' + prevRemoteTail
|
||||||
|
if (norm.endsWith(suffix)) {
|
||||||
|
const stripped = norm.slice(0, -suffix.length)
|
||||||
|
if (!stripped) return ''
|
||||||
|
return stripped.split('/').join(pathSeparator(trimmed))
|
||||||
|
}
|
||||||
|
if (norm === prevRemoteTail) return ''
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将远端最后一级目录名拼到本地输出目录末尾;远端变更时替换旧尾段。 */
|
||||||
|
export function syncLocalPathWithRemote(
|
||||||
|
localPath: string,
|
||||||
|
remotePath: string,
|
||||||
|
prevRemoteTail = '',
|
||||||
|
): { localPath: string; remoteTail: string } {
|
||||||
|
const remoteTail = lastPathSegment(remotePath)
|
||||||
|
const base = stripRemoteTail(localPath, prevRemoteTail)
|
||||||
|
if (!remoteTail) {
|
||||||
|
return { localPath: base || localPath.trim(), remoteTail: '' }
|
||||||
|
}
|
||||||
|
if (!base) {
|
||||||
|
return { localPath: localPath.trim(), remoteTail }
|
||||||
|
}
|
||||||
|
const sep = pathSeparator(base)
|
||||||
|
const normBase = normalizeSlashes(base)
|
||||||
|
if (normBase.endsWith('/' + remoteTail)) {
|
||||||
|
return { localPath: base, remoteTail }
|
||||||
|
}
|
||||||
|
return { localPath: `${base}${sep}${remoteTail}`, remoteTail }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user