| 1 | // -- file extension rules -- |
| 2 | |
| 3 | /** Extensions that must have EXIF/etc data stripped */ |
| 4 | export const extsScrubExif = new Set([ |
| 5 | ".jpg", |
| 6 | ".jpeg", |
| 7 | ".png", |
| 8 | ".mov", |
| 9 | ".mp4", |
| 10 | ".m4a", |
| 11 | ]); |
| 12 | /** Extensions that rendered syntax-highlighted code */ |
| 13 | export const extsCode = new Map<string, highlight.Language>(Object.entries({ |
| 14 | ".json": "json", |
| 15 | ".toml": "toml", |
| 16 | ".ts": "ts", |
| 17 | ".js": "ts", |
| 18 | ".tsx": "tsx", |
| 19 | ".jsx": "tsx", |
| 20 | ".css": "css", |
| 21 | ".py": "python", |
| 22 | ".lua": "lua", |
| 23 | ".sh": "shell", |
| 24 | ".bat": "dosbatch", |
| 25 | ".ps1": "powershell", |
| 26 | ".cmd": "dosbatch", |
| 27 | ".yaml": "yaml", |
| 28 | ".yml": "yaml", |
| 29 | ".zig": "zig", |
| 30 | ".astro": "astro", |
| 31 | ".mdx": "mdx", |
| 32 | ".xml": "xml", |
| 33 | ".jsonc": "json", |
| 34 | ".php": "php", |
| 35 | ".patch": "diff", |
| 36 | ".diff": "diff", |
| 37 | })); |
| 38 | /** These files show an audio embed. */ |
| 39 | export const extsAudio = new Set([ |
| 40 | ".mp3", |
| 41 | ".flac", |
| 42 | ".wav", |
| 43 | ".ogg", |
| 44 | ".m4a", |
| 45 | ]); |
| 46 | /** These files show a video embed. */ |
| 47 | export const extsVideo = new Set([ |
| 48 | ".mp4", |
| 49 | ".mkv", |
| 50 | ".webm", |
| 51 | ".avi", |
| 52 | ".mov", |
| 53 | ]); |
| 54 | /** These files show an image embed */ |
| 55 | export const extsImage = new Set([ |
| 56 | ".jpg", |
| 57 | ".jpeg", |
| 58 | ".png", |
| 59 | ".webp", |
| 60 | ".avif", |
| 61 | ".heic", |
| 62 | ]); |
| 63 | /** These files show an image embed, but aren't optimized */ |
| 64 | export const extsImageLike = new Set([ |
| 65 | ...extsImage, |
| 66 | ".svg", |
| 67 | ".gif", |
| 68 | ]); |
| 69 | |
| 70 | /** These files populate `duration` using `ffprobe` */ |
| 71 | export const extsDuration = new Set([...extsAudio, ...extsVideo]); |
| 72 | /** These files populate `dimensions` using `ffprobe` */ |
| 73 | export const extsDimensions = new Set([...extsImage, ...extsVideo, ".gif"]); |
| 74 | |
| 75 | /** These files read file contents into `contents`, as-is */ |
| 76 | export const extsReadContents = new Set([".txt", ".chat", ".dirsort", ".date"]); |
| 77 | |
| 78 | export const extsArchive = new Set([ |
| 79 | ".zip", |
| 80 | ".rar", |
| 81 | ".7z", |
| 82 | ".tar", |
| 83 | ".gz", |
| 84 | ".bz2", |
| 85 | ".xz", |
| 86 | ]); |
| 87 | |
| 88 | /** |
| 89 | * Formats which are already compression formats, meaning a pass |
| 90 | * through zstd would offer little to negative benefits |
| 91 | */ |
| 92 | export const extsPreCompressed = new Set([ |
| 93 | ...extsAudio, |
| 94 | ...extsVideo, |
| 95 | ...extsImageLike, |
| 96 | ...extsArchive, |
| 97 | ".docx", |
| 98 | ".xlsx", |
| 99 | ".pptx", |
| 100 | ".psd", |
| 101 | ".sketch", |
| 102 | ".ai", |
| 103 | ".3ds", |
| 104 | ".fbx", |
| 105 | ".blend", |
| 106 | ".dng", |
| 107 | ]); |
| 108 | extsPreCompressed.delete(".svg"); |
| 109 | |
| 110 | // files that should never have media processors (transcodes) run on them, |
| 111 | // usually because the file is intentionally cursed. |
| 112 | export const processDenyList = new Set([ |
| 113 | "/2021/top-10000-bread/output.mp4", |
| 114 | ]); |
| 115 | |
| 116 | export function fileIcon( |
| 117 | file: Pick<MediaFile, "kind" | "basename" | "path">, |
| 118 | dirOpen?: boolean, |
| 119 | ) { |
| 120 | const { kind, basename } = file; |
| 121 | if (kind === MediaFileKind.directory) return dirOpen ? "dir-open" : "dir"; |
| 122 | |
| 123 | // -- special cases -- |
| 124 | if (file.path === "/2024/for everyone") return "snow"; |
| 125 | |
| 126 | // -- basename cases -- |
| 127 | if (basename === "readme.txt") return "readme"; |
| 128 | |
| 129 | // -- extension cases -- |
| 130 | const ext = path.extname(file.basename).toLowerCase(); |
| 131 | if ([".comp", ".fuse", ".setting"].includes(ext)) return "fusion"; |
| 132 | if ([".json", ".toml", ".yaml", ".yml"].includes(ext)) return "json"; |
| 133 | if (ext === ".blend") return "blend"; |
| 134 | if (ext === ".chat") return "chat"; |
| 135 | if (ext === ".html") return "webpage"; |
| 136 | if (ext === ".lnk") return "link"; |
| 137 | if (ext === ".txt" || ext === ".md") return "text"; |
| 138 | |
| 139 | // -- extension categories -- |
| 140 | if (extsVideo.has(ext)) return "video"; |
| 141 | if (extsAudio.has(ext)) return "audio"; |
| 142 | if (extsImage.has(ext)) return "image"; |
| 143 | if (extsArchive.has(ext)) return "archive"; |
| 144 | if (extsCode.has(ext)) return "code"; |
| 145 | |
| 146 | return "file"; |
| 147 | } |
| 148 | |
| 149 | import { MediaFile, MediaFileKind } from "#src/file-viewer/models/MediaFile.ts"; |
| 150 | import * as path from "node:path"; |
| 151 | import type * as highlight from "./highlight.ts"; |