diff --git a/API/folder.py b/API/folder.py index 1cba641..19ba405 100644 --- a/API/folder.py +++ b/API/folder.py @@ -16,7 +16,7 @@ import logging import subprocess import shutil from datetime import datetime -from pathlib import Path +from pathlib import Path, PurePosixPath from typing import Optional, Dict, Any, List, Tuple, Set from SYS.utils import sha256_file @@ -159,10 +159,42 @@ class API_folder_store: self.db_path = self.library_root / self.DB_NAME self.connection: Optional[sqlite3.Connection] = None self._init_db() + + def _normalize_input_path(self, file_path: Path) -> Path: + p = Path(file_path).expanduser() + if not p.is_absolute(): + p = self.library_root / p + return p + + def _to_db_file_path(self, file_path: Path) -> str: + """Convert an on-disk file path to a DB-stored relative path (POSIX separators).""" + p = self._normalize_input_path(file_path) + p_abs = p.resolve() + root_abs = self.library_root.resolve() + rel = p_abs.relative_to(root_abs) + rel_posix = PurePosixPath(*rel.parts).as_posix() + rel_posix = str(rel_posix or "").strip() + if not rel_posix or rel_posix == ".": + raise ValueError(f"Invalid relative path for DB storage: {file_path}") + return rel_posix + + def _from_db_file_path(self, db_file_path: str) -> Path: + """Convert a DB-stored relative path (POSIX separators) into an absolute path.""" + rel_str = str(db_file_path or "").strip() + if not rel_str: + raise ValueError("Missing DB file_path") + rel_parts = PurePosixPath(rel_str).parts + return self.library_root / Path(*rel_parts) def _init_db(self) -> None: """Initialize database connection and create tables if needed.""" try: + # Ensure the library root exists; sqlite cannot create parent dirs. + try: + self.library_root.mkdir(parents=True, exist_ok=True) + except Exception as exc: + raise RuntimeError(f"Cannot create/open library root directory: {self.library_root}: {exc}") from exc + # Use check_same_thread=False to allow multi-threaded access # This is safe because we're not sharing connections across threads; # each thread will get its own cursor @@ -530,15 +562,29 @@ class API_folder_store: The file hash (primary key) """ try: - str_path = str(file_path.resolve()) - logger.debug(f"[get_or_create_file_entry] Looking up: {str_path}") + abs_path = self._normalize_input_path(file_path) + db_path = self._to_db_file_path(abs_path) + logger.debug(f"[get_or_create_file_entry] Looking up: {db_path}") # If hash not provided, compute it if not file_hash: - file_hash = sha256_file(file_path) + file_hash = sha256_file(abs_path) logger.debug(f"[get_or_create_file_entry] Computed hash: {file_hash}") cursor = self.connection.cursor() + + # Prefer existing entry by path (file_path is UNIQUE in schema). + cursor.execute("SELECT hash FROM files WHERE file_path = ?", (db_path,)) + row = cursor.fetchone() + if row and row[0]: + existing_hash = str(row[0]) + if existing_hash != file_hash: + logger.debug( + f"[get_or_create_file_entry] Found existing file_path with different hash: path={db_path} existing={existing_hash} computed={file_hash}" + ) + else: + logger.debug(f"[get_or_create_file_entry] Found existing file_path: {db_path} -> {existing_hash}") + return existing_hash # Check if file entry exists cursor.execute("SELECT hash FROM files WHERE hash = ?", (file_hash,)) @@ -549,16 +595,26 @@ class API_folder_store: return file_hash logger.debug(f"[get_or_create_file_entry] File entry not found, creating new one") - stat = file_path.stat() - cursor.execute(""" - INSERT INTO files (hash, file_path, file_modified) - VALUES (?, ?, ?) - """, (file_hash, str_path, stat.st_mtime)) + stat = abs_path.stat() + try: + cursor.execute(""" + INSERT INTO files (hash, file_path, file_modified) + VALUES (?, ?, ?) + """, (file_hash, db_path, stat.st_mtime)) + except sqlite3.IntegrityError: + # Most likely: UNIQUE constraint on file_path. Re-fetch and return. + cursor.execute("SELECT hash FROM files WHERE file_path = ?", (db_path,)) + row2 = cursor.fetchone() + if row2 and row2[0]: + existing_hash = str(row2[0]) + logger.debug(f"[get_or_create_file_entry] Recovered from UNIQUE(file_path): {db_path} -> {existing_hash}") + return existing_hash + raise logger.debug(f"[get_or_create_file_entry] Created new file entry for hash: {file_hash}") # Auto-create title tag - filename_without_ext = file_path.stem + filename_without_ext = abs_path.stem if filename_without_ext: # Normalize underscores to spaces for consistency title_value = filename_without_ext.replace("_", " ").strip() @@ -579,7 +635,8 @@ class API_folder_store: def get_file_hash(self, file_path: Path) -> Optional[str]: """Get the file hash for a file path, or None if not found.""" try: - str_path = str(file_path.resolve()) + abs_path = self._normalize_input_path(file_path) + str_path = self._to_db_file_path(abs_path) cursor = self.connection.cursor() cursor.execute("SELECT hash FROM files WHERE file_path = ?", (str_path,)) row = cursor.fetchone() @@ -761,10 +818,11 @@ class API_folder_store: def save_metadata(self, file_path: Path, metadata: Dict[str, Any]) -> None: """Save metadata for a file.""" try: - str_path = str(file_path.resolve()) - logger.debug(f"[save_metadata] Starting save for: {str_path}") + abs_path = self._normalize_input_path(file_path) + db_path = self._to_db_file_path(abs_path) + logger.debug(f"[save_metadata] Starting save for: {db_path}") - file_hash = self.get_or_create_file_entry(file_path, metadata.get('hash')) + file_hash = self.get_or_create_file_entry(abs_path, metadata.get('hash')) logger.debug(f"[save_metadata] Got/created file_hash: {file_hash}") cursor = self.connection.cursor() @@ -815,10 +873,11 @@ class API_folder_store: def save_file_info(self, file_path: Path, metadata: Dict[str, Any], tags: List[str]) -> None: """Save metadata and tags for a file in a single transaction.""" try: - str_path = str(file_path.resolve()) - logger.debug(f"[save_file_info] Starting save for: {str_path}") + abs_path = self._normalize_input_path(file_path) + db_path = self._to_db_file_path(abs_path) + logger.debug(f"[save_file_info] Starting save for: {db_path}") - file_hash = self.get_or_create_file_entry(file_path, metadata.get('hash')) + file_hash = self.get_or_create_file_entry(abs_path, metadata.get('hash')) cursor = self.connection.cursor() @@ -898,10 +957,11 @@ class API_folder_store: def save_tags(self, file_path: Path, tags: List[str]) -> None: """Save tags for a file, replacing all existing tags.""" try: - str_path = str(file_path.resolve()) - logger.debug(f"[save_tags] Starting save for: {str_path}") + abs_path = self._normalize_input_path(file_path) + db_path = self._to_db_file_path(abs_path) + logger.debug(f"[save_tags] Starting save for: {db_path}") - file_hash = self.get_or_create_file_entry(file_path) + file_hash = self.get_or_create_file_entry(abs_path) logger.debug(f"[save_tags] Got/created file_hash: {file_hash}") cursor = self.connection.cursor() @@ -923,7 +983,7 @@ class API_folder_store: """, (file_hash, existing_title[0])) logger.debug(f"[save_tags] Preserved existing title tag") elif not existing_title and not new_title_provided: - filename_without_ext = file_path.stem + filename_without_ext = abs_path.stem if filename_without_ext: # Normalize underscores to spaces for consistency title_value = filename_without_ext.replace("_", " ").strip() @@ -1325,7 +1385,16 @@ class API_folder_store: LIMIT ? """, (tag, limit)) - return cursor.fetchall() + rows = cursor.fetchall() or [] + results: List[tuple] = [] + for row in rows: + try: + file_hash = str(row[0]) + db_path = str(row[1]) + results.append((file_hash, str(self._from_db_file_path(db_path)))) + except Exception: + continue + return results except Exception as e: logger.error(f"Error searching by tag '{tag}': {e}", exc_info=True) return [] @@ -1340,7 +1409,7 @@ class API_folder_store: """, (file_hash,)) row = cursor.fetchone() - return Path(row[0]) if row else None + return self._from_db_file_path(row[0]) if row else None except Exception as e: logger.error(f"Error searching by hash '{file_hash}': {e}", exc_info=True) return None @@ -1357,8 +1426,10 @@ class API_folder_store: def rename_file(self, old_path: Path, new_path: Path) -> None: """Rename a file in the database, preserving all metadata.""" try: - str_old_path = str(old_path.resolve()) - str_new_path = str(new_path.resolve()) + abs_old = self._normalize_input_path(old_path) + abs_new = self._normalize_input_path(new_path) + str_old_path = self._to_db_file_path(abs_old) + str_new_path = self._to_db_file_path(abs_new) cursor = self.connection.cursor() cursor.execute(""" @@ -1380,7 +1451,11 @@ class API_folder_store: removed_count = 0 for file_hash, file_path in cursor.fetchall(): - if not Path(file_path).exists(): + try: + abs_path = self._from_db_file_path(file_path) + except Exception: + abs_path = Path(file_path) + if not abs_path.exists(): cursor.execute("DELETE FROM files WHERE hash = ?", (file_hash,)) removed_count += 1 @@ -1399,7 +1474,8 @@ class API_folder_store: deleted hash. """ try: - str_path = str(file_path.resolve()) + abs_path = self._normalize_input_path(file_path) + str_path = self._to_db_file_path(abs_path) cursor = self.connection.cursor() # Get the hash first (for logging) @@ -2272,7 +2348,7 @@ class LocalLibraryInitializer: cursor = self.db.connection.cursor() cursor.execute( "UPDATE files SET file_path = ?, updated_at = CURRENT_TIMESTAMP WHERE hash = ?", - (str(target_path.resolve()), file_hash), + (self.db._to_db_file_path(target_path), file_hash), ) except Exception as exc: logger.debug(f"Failed to reset DB path to canonical file for {file_hash}: {exc}") @@ -2309,7 +2385,7 @@ class LocalLibraryInitializer: cursor = self.db.connection.cursor() cursor.execute( "UPDATE files SET file_path = ?, updated_at = CURRENT_TIMESTAMP WHERE hash = ?", - (str(target_path.resolve()), file_hash), + (self.db._to_db_file_path(target_path), file_hash), ) except Exception: pass @@ -2371,7 +2447,11 @@ class LocalLibraryInitializer: result = {} for file_hash, file_path in cursor.fetchall(): - normalized = str(Path(file_path).resolve()).lower() + try: + abs_path = self.db._from_db_file_path(file_path) + except Exception: + abs_path = Path(file_path) + normalized = str(abs_path.resolve()).lower() result[normalized] = file_hash return result diff --git a/MPV/portable_config/mpv.conf b/MPV/portable_config/mpv.conf index 69f5c4d..b76e50a 100644 --- a/MPV/portable_config/mpv.conf +++ b/MPV/portable_config/mpv.conf @@ -17,10 +17,11 @@ auto-window-resize=no ontop=yes -vo=gpu-next # faster with large images -image-display-duration=inf # don't stop displaying images after 5 second -prefetch-playlist # load the next image before viewing it -reset-on-next-file=video-zoom,panscan,video-unscaled,video-align-x,video-align-y,video-rotate # reset zoom, alignment and rotation between images +# Avoid showing embedded cover art for audio-only files. +audio-display=no + +# gpu-next can be fragile on some Windows/D3D11 setups; prefer the stable VO. +vo=gpu # Show this after loading a new file. You can show text permanently instead by setting osd-msg1. osd-playing-msg=${!playlist-count==1:[${playlist-pos-1}/${playlist-count}] }${media-title} ${?width:${width}x${height}} ${?current-tracks/video/image==no:${?percent-pos==0:${duration}}${!percent-pos==0:${time-pos} / ${duration} (${percent-pos}%)}} @@ -44,6 +45,11 @@ auto-window-resize=no # preserve the window size when changing playlist entry profile-cond=get('current-tracks/video', {}).image or (not get('current-tracks/video') and get('user-data/mpv/image')) profile-restore=copy +# Stand-alone images. +image-display-duration=inf # don't stop displaying images after 5 second +prefetch-playlist # load the next image before viewing it +reset-on-next-file=video-zoom,panscan,video-unscaled,video-align-x,video-align-y,video-rotate # reset zoom, alignment and rotation between images + d3d11-flip=no # enable background transparency if on d3d11/Windows video-recenter # recenter after zooming out input-preprocess-wheel=no # pan diagonally with a touchpad diff --git a/Store/Folder.py b/Store/Folder.py index a551eff..03a1545 100644 --- a/Store/Folder.py +++ b/Store/Folder.py @@ -195,7 +195,7 @@ class Folder(Store): try: cursor.execute( "UPDATE files SET file_path = ?, updated_at = CURRENT_TIMESTAMP WHERE hash = ?", - (str(hash_path.resolve()), file_hash), + (db._to_db_file_path(hash_path), file_hash), ) except Exception: pass @@ -237,7 +237,7 @@ class Folder(Store): for file_hash, file_path_str in files_without_title: try: - file_path = Path(file_path_str) + file_path = location_path / str(file_path_str) if file_path.exists(): # Use the filename as the title title_tag = f"title:{file_path.name}" @@ -599,7 +599,7 @@ class Folder(Store): for file_hash, file_path_str, size_bytes, ext in rows: if not file_path_str: continue - file_path = Path(file_path_str) + file_path = search_dir / str(file_path_str) if not file_path.exists(): continue if size_bytes is None: @@ -723,7 +723,7 @@ class Folder(Store): for file_hash, file_path_str, size_bytes, ext in rows: if not file_path_str: continue - file_path = Path(file_path_str) + file_path = search_dir / str(file_path_str) if not file_path.exists(): continue if size_bytes is None: @@ -764,7 +764,7 @@ class Folder(Store): for file_hash, file_path_str, size_bytes, ext in rows: if not file_path_str: continue - file_path = Path(file_path_str) + file_path = search_dir / str(file_path_str) if not file_path.exists(): continue if size_bytes is None: @@ -793,7 +793,7 @@ class Folder(Store): for file_hash, file_path_str, size_bytes, ext in rows: if not file_path_str: continue - file_path = Path(file_path_str) + file_path = search_dir / str(file_path_str) if not file_path.exists(): continue if size_bytes is None: @@ -819,7 +819,7 @@ class Folder(Store): for file_hash, file_path_str, size_bytes, ext in rows: if not file_path_str: continue - file_path = Path(file_path_str) + file_path = search_dir / str(file_path_str) if not file_path.exists(): continue if size_bytes is None: @@ -848,7 +848,7 @@ class Folder(Store): for file_hash, file_path_str, size_bytes, ext in rows: if not file_path_str: continue - file_path = Path(file_path_str) + file_path = search_dir / str(file_path_str) if not file_path.exists(): continue if size_bytes is None: @@ -887,7 +887,7 @@ class Folder(Store): if fnmatch(value, pat): if ext_hashes is not None and file_hash not in ext_hashes: break - file_path = Path(file_path_str) + file_path = search_dir / str(file_path_str) if file_path.exists(): if size_bytes is None: size_bytes = file_path.stat().st_size @@ -958,7 +958,7 @@ class Folder(Store): for file_hash, file_path_str, size_bytes, ext in rows: if not file_path_str: continue - file_path = Path(file_path_str) + file_path = search_dir / str(file_path_str) if not file_path.exists(): continue if size_bytes is None: @@ -984,7 +984,7 @@ class Folder(Store): if file_path_str: if ext_hashes is not None and file_hash not in ext_hashes: continue - file_path = Path(file_path_str) + file_path = search_dir / str(file_path_str) if file_path.exists(): if size_bytes is None: size_bytes = file_path.stat().st_size @@ -1629,7 +1629,7 @@ class Folder(Store): if not p: continue try: - if not Path(p).exists(): + if not (Path(self._location) / p).exists(): continue except Exception: continue diff --git a/cmdnat/out_table.py b/cmdnat/out_table.py new file mode 100644 index 0000000..c5fc778 --- /dev/null +++ b/cmdnat/out_table.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +import os +import re +import sys +from pathlib import Path +from typing import Any, Dict, Sequence, Optional + +from cmdlet._shared import Cmdlet, CmdletArg +from SYS.logger import log +import pipeline as ctx + + +CMDLET = Cmdlet( + name=".out-table", + summary="Save the current result table to an SVG file.", + usage='.out-table -path "C:\\Path\\To\\Dir"', + arg=[ + CmdletArg( + "path", + type="string", + description="Directory (or file path) to write the SVG to", + required=True, + ), + ], + detail=[ + "Exports the most recent table (overlay/stage/last) as an SVG using Rich.", + "Default filename is derived from the table title (sanitized).", + "Examples:", + 'search-store "ext:mp3" | .out-table -path "C:\\Users\\Admin\\Desktop"', + 'search-store "ext:mp3" | .out-table -path "C:\\Users\\Admin\\Desktop\\my-table.svg"', + ], +) + + +_WINDOWS_RESERVED_NAMES = { + "con", + "prn", + "aux", + "nul", + *(f"com{i}" for i in range(1, 10)), + *(f"lpt{i}" for i in range(1, 10)), +} + + +def _sanitize_filename_base(text: str) -> str: + """Sanitize a string for use as a Windows-friendly filename (no extension).""" + s = str(text or "").strip() + if not s: + return "table" + + # Replace characters illegal on Windows (and generally unsafe cross-platform). + s = re.sub(r'[<>:"/\\|?*]', " ", s) + + # Drop control characters. + s = "".join(ch for ch in s if ch.isprintable()) + + # Collapse whitespace. + s = " ".join(s.split()).strip() + + # Windows disallows trailing space/dot. + s = s.rstrip(" .") + + if not s: + s = "table" + + # Avoid reserved device names. + if s.lower() in _WINDOWS_RESERVED_NAMES: + s = f"_{s}" + + # Keep it reasonably short. + if len(s) > 200: + s = s[:200].rstrip(" .") + + return s or "table" + + +def _resolve_output_path(path_arg: str, *, table_title: str) -> Path: + raw = str(path_arg or "").strip() + if not raw: + raise ValueError("-path is required") + + # Treat trailing slash as directory intent even if it doesn't exist yet. + ends_with_sep = raw.endswith((os.sep, os.altsep or "")) + + target = Path(raw) + + if target.exists() and target.is_dir(): + base = _sanitize_filename_base(table_title) + return target / f"{base}.svg" + + if ends_with_sep and not target.suffix: + target.mkdir(parents=True, exist_ok=True) + base = _sanitize_filename_base(table_title) + return target / f"{base}.svg" + + # File path intent. + if not target.suffix: + return target.with_suffix(".svg") + + if target.suffix.lower() != ".svg": + return target.with_suffix(".svg") + + return target + + +def _get_active_table(piped_result: Any) -> Optional[Any]: + # Prefer an explicit ResultTable passed through the pipe, but normally `.out-table` + # is used after `@` which pipes item selections (not the table itself). + if piped_result is not None and hasattr(piped_result, "__rich__"): + # Avoid mistakenly treating a dict/list as a renderable. + if piped_result.__class__.__name__ == "ResultTable": + return piped_result + + return ( + ctx.get_display_table() + or ctx.get_current_stage_table() + or ctx.get_last_result_table() + ) + + +def _run(piped_result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: + args_list = [str(a) for a in (args or [])] + + # Simple flag parsing: `.out-table -path ` + path_arg: Optional[str] = None + i = 0 + while i < len(args_list): + low = args_list[i].strip().lower() + if low in {"-path", "--path"} and i + 1 < len(args_list): + path_arg = args_list[i + 1] + i += 2 + continue + if not args_list[i].startswith("-") and path_arg is None: + # Allow `.out-table ` as a convenience. + path_arg = args_list[i] + i += 1 + + if not path_arg: + log("Missing required -path", file=sys.stderr) + return 1 + + table = _get_active_table(piped_result) + if table is None: + log("No table available to export", file=sys.stderr) + return 1 + + title = getattr(table, "title", None) + title_text = str(title or "table") + + try: + out_path = _resolve_output_path(path_arg, table_title=title_text) + out_path.parent.mkdir(parents=True, exist_ok=True) + + from rich.console import Console + + console = Console(record=True) + console.print(table) + console.save_svg(str(out_path)) + + log(f"Saved table SVG: {out_path}") + return 0 + except Exception as exc: + log(f"Failed to save table SVG: {type(exc).__name__}: {exc}", file=sys.stderr) + return 1 + + +CMDLET.exec = _run diff --git a/docs/cookies.md b/docs/cookies.md new file mode 100644 index 0000000..de16c8a --- /dev/null +++ b/docs/cookies.md @@ -0,0 +1,13 @@ +# Obtain cookies.txt for youtube.com +1. You need a google account, throwaway is fine +2. You need webbrowser extension Get cookies.txt LOCALLY + +Chrome based browser: [cookies.txt LOCALLY](https://chromewebstore.google.com/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc) + +Firefox based browser: [cookies.txt LOCALLY](https://addons.mozilla.org/en-US/firefox/addon/get-cookies-txt-locally/) + +3. open incognito tab and sign into youtube with your account +4. open extension and click on "export all cookies" +5. with the cookies.txt file produced, place that in the project folder + +restart the medios-macina app and verify status for cookies is FOUND \ No newline at end of file diff --git a/docs/img/Available formats.svg b/docs/img/Available formats.svg new file mode 100644 index 0000000..02422c9 --- /dev/null +++ b/docs/img/Available formats.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rich + + + + + + + + + + ╭────────────────────────────────────────────────── Available formats for https://www.youtube.com/watch?v=_23dFb50Z2Y ───────────────────────────────────────────────────╮ + +      #ID          RESOLUTION                      EXT            SIZE             VIDEO                             AUDIO                       + ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  +      1140         audio only                      m4a            0.4mb            none                              mp4a.40.2                   +      2251         audio only                      webm           0.4mb            none                              opus                        +      391          256x144                         mp4            avc1.4d400c                       mp4a.40.5                   +      4160         256x144                         mp4            0.1mb            avc1.4d400c                       none                        +      593          640x360                         mp4            avc1.4d401e                       mp4a.40.2                   +      6134         640x360                         mp4            0.6mb            avc1.4d401e                       none                        +      718          640x360                         mp4            0.8mb            avc1.42001e                       mp4a.40.2                   +      8243         640x360                         webm           0.6mb            vp9                               none                        + +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + diff --git a/docs/img/add-file.svg b/docs/img/add-file.svg new file mode 100644 index 0000000..3c07b81 --- /dev/null +++ b/docs/img/add-file.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rich + + + + + + + + + + ╭────────────────────────────────────────────────────────────────────────────────────────── add-file -store tutorial ──────────────────────────────────────────────────────────────────────────────────────────╮ + +         #TITLE                                                                                                    STORE                               SIZE                 EXT              + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  +         1terry davis - command line                                                                               tutorial                            1 mb                 mkv              + +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + diff --git a/docs/img/add-tag.svg b/docs/img/add-tag.svg new file mode 100644 index 0000000..60272a0 --- /dev/null +++ b/docs/img/add-tag.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rich + + + + + + + + + + ╭────────────────────────────────────────────────────────────────────────────────────── tag: terry davis - command line ───────────────────────────────────────────────────────────────────────────────────────╮ + +              #TAG                                                                                                                                                                                    + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  +              1channel:archeroflusitania                                                                                                                                                              +              2cli                                                                                                                                                                                    +              3creator:archeroflusitania                                                                                                                                                              +              4desktop                                                                                                                                                                                +              5title:terry davis - command line                                                                                                                                                       +              6ubuntu                                                                                                                                                                                 +              7upload_date:20250728                                                                                                                                                                   + +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + diff --git a/docs/img/get-tag.svg b/docs/img/get-tag.svg new file mode 100644 index 0000000..711621d --- /dev/null +++ b/docs/img/get-tag.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rich + + + + + + + + + + ╭────────────────────────────────────────────────────────────────────────────────────── tag: terry davis - command line ───────────────────────────────────────────────────────────────────────────────────────╮ + +              #TAG                                                                                                                                                                                    + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  +              1channel:archeroflusitania                                                                                                                                                              +              2creator:archeroflusitania                                                                                                                                                              +              3title:terry davis - command line                                                                                                                                                       +              4upload_date:20250728                                                                                                                                                                   + +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + diff --git a/docs/img/search-store.svg b/docs/img/search-store.svg new file mode 100644 index 0000000..3c3199d --- /dev/null +++ b/docs/img/search-store.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rich + + + + + + + + + + ╭─────────────────────────────────────────────────────────────────────────────────────────── search-store "ubuntu" ────────────────────────────────────────────────────────────────────────────────────────────╮ + +         #TITLE                                                                                                    STORE                               SIZE                 EXT              + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  +         1terry davis - command line                                                                               tutorial                            1 mb                 mkv              + +╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..51d9671 --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,76 @@ +# set up config.conf +the config file is meant to be modular, you only need to put in what you want. technically you do not need a file store, but you will miss out on a bunch features. For this tutorial we are going to make an example folderstore. Add the below to your config.conf, pick a path for you that is a blank folder non-system folder. + + +
+
config.conf
+
[store=folder]
+name="tutorial"
+path="C:\Users\Admin\Downloads\tutorial"
+
+
+ + +after your done save the file and restart the cli.py + +# Downloading from youtube +### cookies.txt required - [cookies.txt guide](cookies.md) +start up the cli and enter this at prompt, you can copy and paste + +
+
<🜂🜄|🜁🜃>
+
download-media "https://www.youtube.com/watch?v=_23dFb50Z2Y" | add-file -store tutorial
+
+
+ +![Available formats]() +this shows the available formats you can download, the audio is audio only, and the video will automatically merge audio so you only need to pick the video if you want video and audio. it is ordered best quality at the lowest/highest number down. + +the # column is how you select what you want to pick, run this next + +
+
<🜂🜄|🜁🜃>
+
@8 
+
+
+ +![add-file]() + +files inside of stores are renamed as their hash, you will only be able to find your file by searching for it, the title: tag acts as a psuedo filename, if you search "terry" then the file will be brought up, if you search "archeroflusitania" the file will not come up, you need to prepend the namespace first, "channel:archeroflusitania"; you can also use wild card "channel:arch*" and that will pull it up. lets see what tags the file has, run this. + +
+
<🜂🜄|🜁🜃>
+
@1 | get-tag 
+
+
+ +![get-tag]() +these tags are scraped from the youtube video using yt-dlp, they are stored in your store's database, this is how you will actually find your files so make sure it has either a title: tag that you can look up or you add your custom tags +now lets add more tags, run this + +
+
<🜂🜄|🜁🜃>
+
@ | add-tag "cli,ubuntu,desktop"
+
+
+ +![add-tag]() + +we added freeform tags, freeform tags are tags that dont have colons in them, these tags show up in searches without any special prepends. run the following below to search for our new tags added to the file. + +
+
<🜂🜄|🜁🜃>
+
search-store "ubuntu"
+
+
+ +![search-store]() + +to access your file and view it, you can run either + +@1 | get-file + +or if you have mpv installed (the preferred way for video files) + +@1 | .pipe +