···11+// follow-or-create.js — obsidian-vimrc-support jsfile command
22+// Args from plugin: editor, view, selection
33+// Executed via Function() constructor — return value is NOT awaited.
44+55+(async function() {
66+ try {
77+ var app = view.app;
88+ var activeFile = view.file;
99+ var sourcePath = activeFile ? activeFile.path : "";
1010+ var cur = editor.getCursor();
1111+ var line = editor.getLine(cur.line);
1212+1313+ var text = "";
1414+ var rangeFrom = null;
1515+ var rangeTo = null;
1616+ var insideWikilink = false;
1717+1818+ // --- Check if cursor is inside a [[wikilink]] ---
1919+ var before = line.substring(0, cur.ch);
2020+ var after = line.substring(cur.ch);
2121+ var openIdx = before.lastIndexOf("[[");
2222+ var closeInBefore = before.lastIndexOf("]]");
2323+ var closeIdx = after.indexOf("]]");
2424+2525+ if (openIdx !== -1 && (closeInBefore === -1 || closeInBefore < openIdx) && closeIdx !== -1) {
2626+ insideWikilink = true;
2727+ var fullLink = line.substring(openIdx + 2, cur.ch + closeIdx);
2828+ text = fullLink.split("|")[0].trim();
2929+ rangeFrom = { line: cur.line, ch: openIdx };
3030+ rangeTo = { line: cur.line, ch: cur.ch + closeIdx + 2 };
3131+ }
3232+3333+ // --- Visual mode: use the `selection` parameter from the plugin ---
3434+ // The ':' keystroke collapses the editor selection before the ex-command
3535+ // runs, so editor.getSelection() returns "". However, the plugin captures
3636+ // the selection on cursorActivity (before collapse) and passes it as the
3737+ // `selection` argument. We use it when anchor !== head.
3838+ if (!insideWikilink && !text && selection && selection.anchor && selection.head) {
3939+ var a = selection.anchor;
4040+ var h = selection.head;
4141+ if (a.line !== h.line || a.ch !== h.ch) {
4242+ if (a.line < h.line || (a.line === h.line && a.ch <= h.ch)) {
4343+ rangeFrom = { line: a.line, ch: a.ch };
4444+ rangeTo = { line: h.line, ch: h.ch };
4545+ } else {
4646+ rangeFrom = { line: h.line, ch: h.ch };
4747+ rangeTo = { line: a.line, ch: a.ch };
4848+ }
4949+ text = editor.getRange(rangeFrom, rangeTo).trim();
5050+ }
5151+ }
5252+5353+ // --- Normal mode: word under cursor ---
5454+ if (!insideWikilink && !text) {
5555+ var isWordChar = function(c) {
5656+ if (!c) return false;
5757+ if (c === "'" || c === "\u2019") return true; // straight and curly apostrophe
5858+ return /[\p{L}\p{N}_\-]/u.test(c);
5959+ };
6060+ var start = cur.ch;
6161+ var end = cur.ch;
6262+ while (start > 0 && isWordChar(line[start - 1])) start--;
6363+ while (end < line.length && isWordChar(line[end])) end++;
6464+ text = line.substring(start, end);
6565+ rangeFrom = { line: cur.line, ch: start };
6666+ rangeTo = { line: cur.line, ch: end };
6767+ }
6868+6969+ if (!text) return;
7070+7171+ // --- If inside a wikilink, just open it in a new tab ---
7272+ if (insideWikilink) {
7373+ await app.workspace.openLinkText(text, sourcePath, "tab");
7474+ return;
7575+ }
7676+7777+ // --- Sanitize text for use in [[wikilink|alias]] syntax ---
7878+ // Pipe and closing brackets would break the link.
7979+ var safeAlias = text.replace(/\|/g, "-").replace(/\]\]/g, ")");
8080+8181+ // --- Search for existing note by link path OR resolved title ---
8282+ var foundFile = null;
8383+8484+ // First: try standard link-path resolution (handles basenames, paths, etc.)
8585+ foundFile = app.metadataCache.getFirstLinkpathDest(text, sourcePath);
8686+8787+ // Second: use obsidian-front-matter-title plugin's resolver if available.
8888+ // This respects the plugin's configured template (e.g. "title", "foo.bar")
8989+ // so we don't hardcode which frontmatter key holds the display name.
9090+ // Falls back to raw frontmatter.title if the plugin isn't installed.
9191+ if (!foundFile) {
9292+ var fmtPlugin = app.plugins.getPlugin("obsidian-front-matter-title-plugin");
9393+ var resolver = null;
9494+ if (fmtPlugin && fmtPlugin.getDefer) {
9595+ var defer = fmtPlugin.getDefer();
9696+ if (defer && defer.isPluginReady && defer.isPluginReady()) {
9797+ var api = defer.getApi();
9898+ if (api) {
9999+ var factory = api.getResolverFactory();
100100+ if (factory) {
101101+ resolver = factory.createResolver("explorer");
102102+ }
103103+ }
104104+ }
105105+ }
106106+107107+ var allFiles = app.vault.getMarkdownFiles();
108108+ var lowerText = text.toLowerCase();
109109+ for (var i = 0; i < allFiles.length; i++) {
110110+ var resolved = null;
111111+ if (resolver) {
112112+ resolved = resolver.resolve(allFiles[i].path);
113113+ }
114114+ if (!resolved) {
115115+ var cache = app.metadataCache.getFileCache(allFiles[i]);
116116+ resolved = cache && cache.frontmatter && cache.frontmatter.title
117117+ ? String(cache.frontmatter.title)
118118+ : null;
119119+ }
120120+ if (resolved && resolved.toLowerCase() === lowerText) {
121121+ foundFile = allFiles[i];
122122+ break;
123123+ }
124124+ }
125125+ }
126126+127127+ if (foundFile) {
128128+ var linkStr = foundFile.basename === text
129129+ ? "[[" + text + "]]"
130130+ : "[[" + foundFile.basename + "|" + safeAlias + "]]";
131131+ editor.replaceRange(linkStr, rangeFrom, rangeTo);
132132+ await app.workspace.openLinkText(foundFile.path, sourcePath, "tab");
133133+ return;
134134+ }
135135+136136+ // --- Create a new note with zk-prefixer style ID ---
137137+ var now = new Date();
138138+ var pad = function(n, w) { return String(n).padStart(w, "0"); };
139139+ var yy = pad(now.getFullYear() % 100, 2);
140140+ var mm = pad(now.getMonth() + 1, 2);
141141+ var dd = pad(now.getDate(), 2);
142142+ var hh = pad(now.getHours(), 2);
143143+ var mi = pad(now.getMinutes(), 2);
144144+ var zkId = yy + mm + dd + "-" + hh + mi;
145145+ var fileName = zkId + ".md";
146146+147147+ // Handle collision: if file with same zkId exists, append a letter suffix
148148+ var suffix = "";
149149+ var alphabet = "abcdefghijklmnopqrstuvwxyz";
150150+ while (app.vault.getAbstractFileByPath(fileName)) {
151151+ if (suffix === "") {
152152+ suffix = "a";
153153+ } else {
154154+ var idx = alphabet.indexOf(suffix);
155155+ if (idx >= alphabet.length - 1) {
156156+ throw new Error("Too many notes created this minute (" + zkId + "a-z exhausted)");
157157+ }
158158+ suffix = alphabet[idx + 1];
159159+ }
160160+ fileName = zkId + suffix + ".md";
161161+ }
162162+ var noteId = suffix ? zkId + suffix : zkId;
163163+164164+ // Sanitize title for YAML: backslashes first, then double quotes, then newlines
165165+ var safeTitle = text
166166+ .replace(/\\/g, "\\\\")
167167+ .replace(/"/g, '\\"')
168168+ .replace(/[\r\n]+/g, " ");
169169+170170+ var frontmatter = [
171171+ "---",
172172+ 'title: "' + safeTitle + '"',
173173+ "tags:",
174174+ 'date: "' + zkId + '"',
175175+ "update:",
176176+ "---",
177177+ "",
178178+ ].join("\n");
179179+180180+ // Do editor replacement BEFORE async vault operation to avoid stale positions.
181181+ // Save original text for rollback if vault.create fails.
182182+ var originalText = editor.getRange(rangeFrom, rangeTo);
183183+ var linkStr = "[[" + noteId + "|" + safeAlias + "]]";
184184+ editor.replaceRange(linkStr, rangeFrom, rangeTo);
185185+186186+ try {
187187+ await app.vault.create(fileName, frontmatter);
188188+ } catch(createErr) {
189189+ // Rollback: restore original text since the note wasn't created
190190+ var linkEnd = {
191191+ line: rangeFrom.line,
192192+ ch: rangeFrom.ch + linkStr.length
193193+ };
194194+ editor.replaceRange(originalText, rangeFrom, linkEnd);
195195+ throw createErr;
196196+ }
197197+198198+ await app.workspace.openLinkText(noteId, sourcePath, "tab");
199199+200200+ } catch(e) {
201201+ console.error("followOrCreate error:", e);
202202+ try { new Notice("followOrCreate: " + e.message, 5000); } catch(_) {}
203203+ }
204204+})();
+105
home/profiles/obsidian/obsidian.vimrc
···11+" ============================================================
22+" Obsidian Vimrc (requires Vimrc Support plugin by esm7)
33+" ============================================================
44+55+" --- Clipboard ---
66+" All yanks go to system clipboard
77+set clipboard=unnamedplus
88+99+" Make Y yank to end of line (consistent with C and D)
1010+nnoremap Y y$
1111+1212+" --- Unbind Space for use as leader ---
1313+unmap <Space>
1414+1515+" --- Sidebar & Navigation ---
1616+" Toggle left sidebar
1717+exmap toggleSidebar obcommand app:toggle-left-sidebar
1818+nmap <Space>e :toggleSidebar<CR>
1919+2020+" Reveal active file in file explorer
2121+exmap revealFile obcommand file-explorer:reveal-active-file
2222+nmap <Space>f :revealFile<CR>
2323+2424+" Command palette
2525+exmap commandPalette obcommand command-palette:open
2626+nmap <Space>p :commandPalette<CR>
2727+2828+" --- Tab Navigation ---
2929+" Next tab (matches Vim gt)
3030+exmap nextTab obcommand workspace:next-tab
3131+nmap gt :nextTab<CR>
3232+3333+" Previous tab (matches Vim gT)
3434+exmap prevTab obcommand workspace:previous-tab
3535+nmap gT :prevTab<CR>
3636+3737+" Close current tab
3838+exmap closeTab obcommand workspace:close
3939+nmap gd :closeTab<CR>
4040+4141+" Jump past frontmatter and enter insert mode
4242+exmap goBody jscommand { var lines = editor.getValue().split("\n"); var count = 0; for (var i = 0; i < lines.length; i++) { if (lines[i].trim() === "---") { count++; if (count === 2) { editor.setCursor(i + 1, 0); return; } } } editor.setCursor(0, 0); }
4343+nmap gi :goBody<CR>i
4444+4545+" --- Notes ---
4646+" Daily note (Periodic Notes)
4747+exmap dailyNote obcommand periodic-notes:open-daily-note
4848+nmap <Space>d :dailyNote<CR>
4949+5050+" Weekly note (Periodic Notes)
5151+exmap weeklyNote obcommand periodic-notes:open-weekly-note
5252+nmap <Space>w :weeklyNote<CR>
5353+5454+" New Zettelkasten note
5555+exmap newZK obcommand zk-prefixer
5656+nmap <Space>zn :newZK<CR>
5757+5858+" Split (QuickAdd macro)
5959+exmap splitNote obcommand quickadd:choice:1d350c80-9184-4f8a-b37e-4e7c0712a19f
6060+nmap <Space>zs :splitNote<CR>
6161+6262+" --- Folding ---
6363+exmap unfoldAtCursor obcommand editor:unfold
6464+exmap foldAtCursor obcommand editor:fold
6565+exmap toggleFold obcommand editor:toggle-fold
6666+exmap unfoldAll obcommand editor:unfold-all
6767+exmap foldAll obcommand editor:fold-all
6868+6969+nmap zo :unfoldAtCursor<CR>
7070+nmap zc :foldAtCursor<CR>
7171+nmap za :toggleFold<CR>
7272+nmap zR :unfoldAll<CR>
7373+nmap zM :foldAll<CR>
7474+7575+" --- Spelling ---
7676+" Open spelling suggestions via the editor suggest/context menu
7777+exmap spellcheck jscommand { var pos = view.editor.cm.coordsAtPos(view.editor.cm.state.selection.main.head); view.editor.cm.dom.dispatchEvent(new MouseEvent("contextmenu", {bubbles: true, cancelable: true, clientX: pos.left, clientY: pos.top})); }
7878+nmap z= :spellcheck<CR>
7979+8080+" --- Quick Switcher ---
8181+exmap quickSwitcher obcommand switcher:open
8282+nmap <Space><Space> :quickSwitcher<CR>
8383+8484+" --- Search ---
8585+exmap globalSearch obcommand global-search:open
8686+nmap <Space>fg :globalSearch<CR>
8787+8888+" --- Surround (vim-surround style) ---
8989+" The plugin's built-in surroundOperator acts as a Vim operator:
9090+" ys{motion}{char} in normal mode (e.g. ysiw" to surround word with quotes)
9191+" S{char} in visual mode (e.g. viwS( to surround selection with parens)
9292+" It opens a prompt for the surround character; brackets auto-match.
9393+nunmap s
9494+vunmap s
9595+nmap ys <A-y>s
9696+vmap S <A-y>s
9797+9898+" --- Follow or Create Note ---
9999+" Enter in normal/visual mode: if on a [[wikilink]], open it in a new tab.
100100+" Otherwise, check if a note with the word/selection as title exists:
101101+" - If it does, wrap in [[link]] and open in a new tab.
102102+" - If not, create a new zk-prefixed note with that title and link to it.
103103+exmap followOrCreate jsfile scripts/follow-or-create.js
104104+nmap <CR> :followOrCreate<CR>
105105+vmap <CR> :followOrCreate<CR>
+63-25
home/profiles/opencode/default.nix
···1818 # github-mcp-server binary path from nixpkgs
1919 githubMcpServer = "${pkgs.github-mcp-server}/bin/github-mcp-server";
20202121+<<<<<<< HEAD
2122 # Plugin sources fetched via flake inputs
2223 handoffSrc = inputs.opencode-handoff;
2424+=======
2525+ # Upstream plugin/skill sources pinned via fetchFromGitHub
2626+ opencode-handoff-src = pkgs.fetchFromGitHub {
2727+ owner = "Chickensoupwithrice";
2828+ repo = "opencode-handoff";
2929+ rev = "b18d546e567c8c15c7ce8377f82f1b81cd838890";
3030+ hash = "sha256-1wDwwz7gcKLeCr0kqwQtQi7UWf12AYiPDL8YT9iFO08=";
3131+ };
3232+3333+ learning-opportunities-src = pkgs.fetchFromGitHub {
3434+ owner = "DrCatHicks";
3535+ repo = "learning-opportunities";
3636+ rev = "e5f985d376461993253d285096ed0f4b4a095858";
3737+ hash = "sha256-xMpy9XxMaNCIAOr2dffrc5dyRt56jlam+XQjrNapsEw=";
3838+ };
3939+4040+ learning-goal-src = pkgs.fetchFromGitHub {
4141+ owner = "DrCatHicks";
4242+ repo = "learning-goal";
4343+ rev = "cc7e3a6c7f0917501f1fd422bad81ab6f5040050";
4444+ hash = "sha256-lvQdDRuC9H+8F4Fud753c6NogVdZtGqgtjKbiSVZCig=";
4545+ };
4646+4747+ # Combined plugins derivation: assembles all local and upstream plugins
4848+ # into a single directory with the layout OpenCode expects.
4949+ #
5050+ # OpenCode globs plugins/*.{ts,js} and calls every export as a plugin
5151+ # initializer, so only actual entry points belong at the top level.
5252+ # Helper modules (tools.ts, files.ts, etc.) go in subdirectories to
5353+ # avoid being auto-loaded.
5454+ opencode-plugins = pkgs.runCommand "opencode-plugins" { } ''
5555+ mkdir -p $out/handoff-src
5656+5757+ # opencode-handoff: thin entry point re-exports from a subdirectory
5858+ # so OpenCode only auto-loads the plugin, not its helper modules
5959+ cat > $out/handoff.ts <<'ENTRY'
6060+ export { HandoffPlugin } from "./handoff-src/plugin"
6161+ ENTRY
6262+ ln -s ${opencode-handoff-src}/src/* $out/handoff-src/
6363+6464+ # learning-opportunities-auto: OpenCode port of the upstream
6565+ # Claude Code PostToolUse hook (local file, not a patch of upstream)
6666+ cp ${./plugins/learning-opportunities-auto.js} $out/learning-opportunities-auto.js
6767+ '';
6868+>>>>>>> 64f30fc4d82e01dc9b381096bb4cc8cd5498c6c4
2369in
2470{
2571 home.packages = [
···68114 "opencode/themes".source = ./themes;
69115 "opencode/agents".source = ./agents;
70116 "opencode/commands".source = ./commands;
117117+<<<<<<< HEAD
71118 "opencode/skills".source = ./skills;
119119+=======
120120+121121+ # Local skills
122122+ "opencode/skills/tmux".source = ./skills/tmux;
123123+ "opencode/skills/session-search.disabled".source = ./skills/session-search.disabled;
124124+125125+ # Upstream skills via fetchFromGitHub
126126+ "opencode/skills/learning-opportunities".source =
127127+ "${learning-opportunities-src}/learning-opportunities/skills/learning-opportunities";
128128+ "opencode/skills/learning-goal".source = "${learning-goal-src}/learning-goal/skills/learning-goal";
129129+ "opencode/skills/orient".source = "${learning-opportunities-src}/orient/skills/orient";
130130+131131+ # Plugins: combined derivation with correct layout for OpenCode's
132132+ # plugin loader (only entry points at top level)
133133+ "opencode/plugins".source = opencode-plugins;
134134+>>>>>>> 64f30fc4d82e01dc9b381096bb4cc8cd5498c6c4
72135 };
7313674137 home.file = lib.mkIf isBox {
75138 "usr/.opencode/agents.md".source = ./agents/box.md;
76139 };
7777-7878- # Copy plugin files into the writable plugins directory rather than symlinking.
7979- # Bun's import() resolves symlinks to their real path (inside the read-only Nix
8080- # store), then searches for node_modules/ relative to that real path -- which fails
8181- # because node_modules/ lives at ~/.config/opencode/node_modules/, not in the store.
8282- # Copying the file ensures it physically lives alongside node_modules/ so Bun's
8383- # module resolution can find dependencies if they're ever needed.
8484- home.activation.opencode-plugins = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
8585- mkdir -p "$HOME/.config/opencode/plugins"
8686- mkdir -p "$HOME/.config/opencode/plugins/handoff"
8787- # Local plugins (top-level files are auto-loaded by OpenCode)
8888- install -m644 ${./plugins/learning-opportunities-auto.js} \
8989- "$HOME/.config/opencode/plugins/learning-opportunities-auto.js"
9090- # opencode-handoff: copy entry point and patch imports to use subdir.
9191- # OpenCode globs plugins/*.{ts,js} so only handoff.ts is auto-loaded.
9292- ${pkgs.gnused}/bin/sed \
9393- -e 's|from "./tools"|from "./handoff/tools"|' \
9494- -e 's|from "./files"|from "./handoff/files"|' \
9595- ${handoffSrc}/src/plugin.ts > "$HOME/.config/opencode/plugins/handoff.ts"
9696- cp -f ${handoffSrc}/src/tools.ts "$HOME/.config/opencode/plugins/handoff/tools.ts"
9797- cp -f ${handoffSrc}/src/files.ts "$HOME/.config/opencode/plugins/handoff/files.ts"
9898- cp -f ${handoffSrc}/src/vendor.ts "$HOME/.config/opencode/plugins/handoff/vendor.ts"
9999- chmod 644 "$HOME/.config/opencode/plugins/handoff.ts" \
100100- "$HOME/.config/opencode/plugins/handoff"/*.ts
101101- '';
102140}
···88TRACKING_FILE="/tank/media/tv/.downloaded_shows"
99LOG_FILE="/tank/media/tv/download-log"
1010LOCK_FILE="/tmp/get-tv-sync.lock"
1111-SONARR_API_KEY="PLACEHOLDER"
1111+SONARR_API_KEY="9dba6d5f31eb42cf8c65f4f6f21cc8d2"
1212SONARR_URL="http://localhost:8989"
13131414umask 002
···1818touch "$TRACKING_FILE"
19192020log() { echo "$(date): $1" >>"$LOG_FILE"; }
2121+2222+# Extract a match key from a release name for fuzzy matching.
2323+# Sonarr's sourceTitle often differs from the actual filename on the seedbox:
2424+# - sourceTitle omits file extensions (.mkv, .ts, etc.)
2525+# - sourceTitle omits episode titles that appear in the actual filename
2626+# We match on episode identifier + release group, which are consistent on both sides.
2727+# Returns "episode_id|||group" e.g. "Top.Chef.S23E01|||RAWR" or "Jeopardy.2026.03.19|||BTN"
2828+match_key() {
2929+ local name="$1"
3030+ # Strip known video file extensions
3131+ name="${name%.mkv}"
3232+ name="${name%.ts}"
3333+ name="${name%.mp4}"
3434+ name="${name%.avi}"
3535+ name="${name%.m4v}"
3636+3737+ local ep_id="" group=""
3838+3939+ # Extract release group: everything after the last hyphen
4040+ if [[ "$name" == *-* ]]; then
4141+ group="${name##*-}"
4242+ fi
4343+4444+ # Extract episode identifier: show name + S##E##, YYYY.MM.DD, or S## pattern
4545+ if [[ "$name" =~ ^(.*\.[Ss][0-9]+[Ee][0-9]+) ]]; then
4646+ ep_id="${BASH_REMATCH[1]}"
4747+ elif [[ "$name" =~ ^(.*\.[0-9]{4}\.[0-9]{2}\.[0-9]{2}) ]]; then
4848+ ep_id="${BASH_REMATCH[1]}"
4949+ elif [[ "$name" =~ ^(.*\.[Ss][0-9]+)\. ]]; then
5050+ ep_id="${BASH_REMATCH[1]}"
5151+ fi
5252+5353+ if [ -n "$ep_id" ] && [ -n "$group" ]; then
5454+ echo "${ep_id}|||${group}"
5555+ fi
5656+}
21572258# Acquire exclusive lock to prevent concurrent runs
2359exec 9>"$LOCK_FILE"
···136172 continue
137173 fi
138174139139- # Check if it exists on the remote seedbox (exact match on entry name)
140140- # Use ENVIRON to pass source_title to awk, avoiding backslash interpretation from -v
141141- REMOTE_MATCH=$(SOURCE_TITLE="$source_title" awk -F'\t' 'BEGIN { name = ENVIRON["SOURCE_TITLE"] } $2 == name { print; exit }' "$REMOTE_LISTING_FILE") || true
175175+ # Match sourceTitle against remote seedbox entries using episode ID + release group.
176176+ # Sonarr's sourceTitle often differs from the actual filename (missing extension,
177177+ # missing episode title in the name), so exact matching doesn't work.
178178+ SOURCE_KEY=$(match_key "$source_title")
179179+ if [ -z "$SOURCE_KEY" ]; then
180180+ log "WARNING: Could not extract match key from sourceTitle: $source_title"
181181+ continue
182182+ fi
183183+184184+ REMOTE_MATCH=""
185185+ while IFS=$'\t' read -r rtype rname; do
186186+ [ -z "$rname" ] && continue
187187+ REMOTE_KEY=$(match_key "$rname")
188188+ if [ "$SOURCE_KEY" = "$REMOTE_KEY" ]; then
189189+ REMOTE_MATCH="$rtype"$'\t'"$rname"
190190+ break
191191+ fi
192192+ done <"$REMOTE_LISTING_FILE"
193193+142194 if [ -z "$REMOTE_MATCH" ]; then
143195 log "Not yet on seedbox (still downloading?): $source_title"
144196 continue
···146198147199 # Determine if it's a directory or file from the listing
148200 ENTRY_TYPE=$(echo "$REMOTE_MATCH" | cut -f1)
201201+ REMOTE_NAME=$(echo "$REMOTE_MATCH" | cut -f2)
149202150150- log "Downloading '$source_title' -> '$series_folder/'"
203203+ log "Downloading '$source_title' (remote: '$REMOTE_NAME') -> '$series_folder/'"
151204 mkdir -p "$LOCAL_PATH/$series_folder"
152205153206 if [ "$ENTRY_TYPE" = "d" ]; then
154207 # Directory: rsync recursively, trailing slash to put contents into series folder
155208 if rsync -rs --partial --timeout=600 --log-file="$LOG_FILE" \
156156- "$REMOTE_HOST:$REMOTE_PATH$source_title/" \
209209+ "$REMOTE_HOST:$REMOTE_PATH$REMOTE_NAME/" \
157210 "$LOCAL_PATH/$series_folder/"; then
158211 echo "$source_title" >>"$TRACKING_FILE"
159159- log "Successfully downloaded directory: $source_title"
212212+ log "Successfully downloaded directory: $REMOTE_NAME"
160213 DOWNLOADED=$((DOWNLOADED + 1))
161214 else
162162- log "Failed to download directory: $source_title"
215215+ log "Failed to download directory: $REMOTE_NAME"
163216 fi
164217 else
165218 # Single file: rsync into series folder
166219 if rsync -s --partial --timeout=600 --log-file="$LOG_FILE" \
167167- "$REMOTE_HOST:$REMOTE_PATH$source_title" \
220220+ "$REMOTE_HOST:$REMOTE_PATH$REMOTE_NAME" \
168221 "$LOCAL_PATH/$series_folder/"; then
169222 echo "$source_title" >>"$TRACKING_FILE"
170170- log "Successfully downloaded file: $source_title"
223223+ log "Successfully downloaded file: $REMOTE_NAME"
171224 DOWNLOADED=$((DOWNLOADED + 1))
172225 else
173173- log "Failed to download file: $source_title"
226226+ log "Failed to download file: $REMOTE_NAME"
174227 fi
175228 fi
176229done <<<"$RECORDS"