pstream is dead; long live pstream
taciturnaxolotl.github.io/pstream-ng/
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}