pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
at main 242 lines 6.5 kB view raw
1import { BookmarkMediaItem } from "@/stores/bookmarks"; 2 3/** 4 * Options for modifying bookmark properties 5 */ 6export interface BookmarkModificationOptions { 7 /** Update the title of the bookmark */ 8 title?: string; 9 /** Update the year of the bookmark */ 10 year?: number; 11 /** Update the poster URL of the bookmark */ 12 poster?: string; 13 /** Update the groups array (replaces existing groups) */ 14 groups?: string[]; 15 /** Add groups to existing groups (doesn't remove existing ones) */ 16 addGroups?: string[]; 17 /** Remove specific groups from the bookmark */ 18 removeGroups?: string[]; 19 /** Update favorite episodes */ 20 favoriteEpisodes?: string[]; 21} 22 23/** 24 * Result of a bookmark modification operation 25 */ 26export interface BookmarkModificationResult { 27 /** IDs of bookmarks that were modified */ 28 modifiedIds: string[]; 29 /** Whether any bookmarks were actually changed */ 30 hasChanges: boolean; 31} 32 33/** 34 * Modifies a single bookmark item with the provided options 35 */ 36export function modifyBookmark( 37 bookmark: BookmarkMediaItem, 38 options: BookmarkModificationOptions, 39): BookmarkMediaItem { 40 const modified = { ...bookmark, updatedAt: Date.now() }; 41 42 if (options.title !== undefined) { 43 modified.title = options.title; 44 } 45 46 if (options.year !== undefined) { 47 modified.year = options.year; 48 } 49 50 if (options.poster !== undefined) { 51 modified.poster = options.poster; 52 } 53 54 if (options.groups !== undefined) { 55 modified.group = options.groups; 56 } 57 58 if (options.addGroups && options.addGroups.length > 0) { 59 const currentGroups = modified.group || []; 60 const newGroups = [...currentGroups]; 61 options.addGroups.forEach((group) => { 62 if (!newGroups.includes(group)) { 63 newGroups.push(group); 64 } 65 }); 66 modified.group = newGroups; 67 } 68 69 if (options.removeGroups && options.removeGroups.length > 0) { 70 const currentGroups = modified.group || []; 71 modified.group = currentGroups.filter( 72 (group) => !options.removeGroups!.includes(group), 73 ); 74 } 75 76 if (options.favoriteEpisodes !== undefined) { 77 modified.favoriteEpisodes = options.favoriteEpisodes; 78 } 79 80 return modified; 81} 82 83/** 84 * Modifies multiple bookmarks by their IDs 85 */ 86export function modifyBookmarks( 87 bookmarks: Record<string, BookmarkMediaItem>, 88 bookmarkIds: string[], 89 options: BookmarkModificationOptions, 90): { 91 modifiedBookmarks: Record<string, BookmarkMediaItem>; 92 result: BookmarkModificationResult; 93} { 94 const modifiedBookmarks = { ...bookmarks }; 95 const modifiedIds: string[] = []; 96 let hasChanges = false; 97 98 bookmarkIds.forEach((id) => { 99 const original = modifiedBookmarks[id]; 100 if (original) { 101 const modified = modifyBookmark(original, options); 102 modifiedBookmarks[id] = modified; 103 modifiedIds.push(id); 104 105 // Check if anything actually changed 106 if (!hasChanges) { 107 hasChanges = Object.keys(options).some((key) => { 108 const optionKey = key as keyof BookmarkModificationOptions; 109 if (optionKey === "addGroups" || optionKey === "removeGroups") 110 return true; 111 112 const optionValue = options[optionKey]; 113 const currentValue = modified[optionKey as keyof BookmarkMediaItem]; 114 115 if (Array.isArray(optionValue) && Array.isArray(currentValue)) { 116 return ( 117 optionValue.length !== currentValue.length || 118 !optionValue.every((val) => currentValue.includes(val)) 119 ); 120 } 121 122 return optionValue !== currentValue; 123 }); 124 } 125 } 126 }); 127 128 return { 129 modifiedBookmarks, 130 result: { modifiedIds, hasChanges: hasChanges && modifiedIds.length > 0 }, 131 }; 132} 133 134/** 135 * Options for bulk group modifications 136 */ 137export interface BulkGroupModificationOptions { 138 /** The old group name to replace */ 139 oldGroupName: string; 140 /** The new group name */ 141 newGroupName: string; 142 /** Whether to only modify bookmarks that have this as their only group */ 143 onlyIfExclusive?: boolean; 144} 145 146/** 147 * Modifies all bookmarks that contain a specific group name 148 */ 149export function modifyBookmarksByGroup( 150 bookmarks: Record<string, BookmarkMediaItem>, 151 options: BulkGroupModificationOptions, 152): { 153 modifiedBookmarks: Record<string, BookmarkMediaItem>; 154 result: BookmarkModificationResult; 155} { 156 const modifiedBookmarks = { ...bookmarks }; 157 const modifiedIds: string[] = []; 158 159 Object.entries(bookmarks).forEach(([id, bookmark]) => { 160 if (bookmark.group && bookmark.group.includes(options.oldGroupName)) { 161 // Check if we should only modify exclusive groups 162 if (options.onlyIfExclusive && bookmark.group.length > 1) { 163 return; 164 } 165 166 const newGroups = bookmark.group.map((group) => 167 group === options.oldGroupName ? options.newGroupName : group, 168 ); 169 170 modifiedBookmarks[id] = { 171 ...bookmark, 172 group: newGroups, 173 updatedAt: Date.now(), 174 }; 175 modifiedIds.push(id); 176 } 177 }); 178 179 return { 180 modifiedBookmarks, 181 result: { modifiedIds, hasChanges: modifiedIds.length > 0 }, 182 }; 183} 184 185/** 186 * Finds all bookmarks that belong to a specific group 187 */ 188export function findBookmarksByGroup( 189 bookmarks: Record<string, BookmarkMediaItem>, 190 groupName: string, 191): string[] { 192 return Object.entries(bookmarks) 193 .filter(([, bookmark]) => bookmark.group?.includes(groupName)) 194 .map(([id]) => id); 195} 196 197/** 198 * Gets all unique group names from bookmarks 199 */ 200export function getAllGroupNames( 201 bookmarks: Record<string, BookmarkMediaItem>, 202): string[] { 203 const groups = new Set<string>(); 204 Object.values(bookmarks).forEach((bookmark) => { 205 if (bookmark.group) { 206 bookmark.group.forEach((group) => groups.add(group)); 207 } 208 }); 209 return Array.from(groups); 210} 211 212/** 213 * Validates a group name format 214 */ 215export function isValidGroupName(groupName: string): boolean { 216 // Group names should be non-empty and not contain only whitespace 217 return groupName.trim().length > 0; 218} 219 220/** 221 * Parses a group string to extract icon and name components 222 */ 223export function parseGroupString(group: string): { 224 icon: string; 225 name: string; 226} { 227 const match = group.match(/^\[([a-zA-Z0-9_]+)\](.*)$/); 228 if (match) { 229 return { icon: match[1], name: match[2].trim() }; 230 } 231 return { icon: "", name: group }; 232} 233 234/** 235 * Creates a formatted group string from icon and name 236 */ 237export function createGroupString(icon: string, name: string): string { 238 if (icon && name) { 239 return `[${icon}]${name}`; 240 } 241 return name; 242}