pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix watch history issues

Pas 04f59589 bd717b6b

+175 -31
+14
src/backend/accounts/import.ts
··· 6 6 import { BookmarkInput } from "./bookmarks"; 7 7 import { ProgressInput } from "./progress"; 8 8 import { SettingsInput } from "./settings"; 9 + import { WatchHistoryInput, watchHistoryItemsToInputs } from "./watchHistory"; 9 10 10 11 export function importProgress( 11 12 url: string, ··· 41 42 return ofetch<void>(`/users/${account.userId}/group-order`, { 42 43 method: "PUT", 43 44 body: groupOrder, 45 + baseURL: url, 46 + headers: getAuthHeaders(account.token), 47 + }); 48 + } 49 + 50 + export function importWatchHistory( 51 + url: string, 52 + account: AccountWithToken, 53 + watchHistoryItems: WatchHistoryInput[], 54 + ) { 55 + return ofetch<void>(`/users/${account.userId}/watch-history/import`, { 56 + method: "PUT", 57 + body: watchHistoryItems, 44 58 baseURL: url, 45 59 headers: getAuthHeaders(account.token), 46 60 });
+8
src/backend/accounts/watchHistory.ts
··· 76 76 }; 77 77 } 78 78 79 + export function watchHistoryItemsToInputs( 80 + watchHistoryItems: Record<string, WatchHistoryItem>, 81 + ): WatchHistoryInput[] { 82 + return Object.entries(watchHistoryItems).map(([id, item]) => 83 + watchHistoryItemToInputs(id, item), 84 + ); 85 + } 86 + 79 87 export async function setWatchHistory( 80 88 url: string, 81 89 account: AccountWithToken,
+2 -1
src/components/overlays/detailsModal/components/carousels/EpisodeCarousel.tsx
··· 179 179 180 180 // If watched (>90%), reset to 0%, otherwise set to 100% 181 181 const isWatched = percentage > 90; 182 + const shouldMarkWatched = !isWatched; 182 183 183 184 // Get the poster URL from the mediaPosterUrl prop 184 185 const posterUrl = mediaPosterUrl; ··· 203 204 }, 204 205 }, 205 206 progress: { 206 - watched: isWatched ? 0 : 60, 207 + watched: shouldMarkWatched ? 60 : 0, // 60 seconds (100%) for watched, 0 for unwatched 207 208 duration: 60, 208 209 }, 209 210 });
+3 -2
src/components/overlays/detailsModal/components/layout/DetailsContent.tsx
··· 208 208 // Get the poster URL from the data 209 209 const posterUrl = data.posterUrl; 210 210 211 - // Update progress - if watched, set to 0%, otherwise set to 100% 211 + // Update progress - if watched, set to 0%, otherwise set to 100% (completed) 212 + const shouldMarkWatched = !isMovieWatched; 212 213 updateItem({ 213 214 meta: { 214 215 tmdbId: data.id.toString(), ··· 220 221 poster: posterUrl, 221 222 }, 222 223 progress: { 223 - watched: isMovieWatched ? 0 : 60, // 60 seconds for "watched" 224 + watched: shouldMarkWatched ? 60 : 0, // 60 seconds (100%) for watched, 0 for unwatched 224 225 duration: 60, 225 226 }, 226 227 });
+11 -1
src/hooks/auth/useMigration.ts
··· 16 16 importGroupOrder, 17 17 importProgress, 18 18 importSettings, 19 + importWatchHistory, 19 20 } from "@/backend/accounts/import"; 21 + import { watchHistoryItemsToInputs } from "@/backend/accounts/watchHistory"; 20 22 // import { getLoginChallengeToken, loginAccount } from "@/backend/accounts/login"; 21 23 import { progressMediaItemToInputs } from "@/backend/accounts/progress"; 22 24 import { ··· 39 41 import { usePreferencesStore } from "@/stores/preferences"; 40 42 import { ProgressMediaItem, useProgressStore } from "@/stores/progress"; 41 43 import { useSubtitleStore } from "@/stores/subtitles"; 44 + import { WatchHistoryItem, useWatchHistoryStore } from "@/stores/watchHistory"; 42 45 43 46 export interface RegistrationData { 44 47 recaptchaToken?: string; ··· 63 66 export function useMigration() { 64 67 const currentAccount = useAuthStore((s) => s.account); 65 68 const progress = useProgressStore((s) => s.items); 69 + const watchHistory = useWatchHistoryStore((s) => s.items); 66 70 const bookmarks = useBookmarkStore((s) => s.bookmarks); 67 71 const groupOrder = useGroupOrderStore((s) => s.groupOrder); 68 72 const preferences = usePreferencesStore.getState(); ··· 77 81 backendUrlInner: string, 78 82 account: AccountWithToken, 79 83 progressItems: Record<string, ProgressMediaItem>, 84 + watchHistoryItems: Record<string, WatchHistoryItem>, 80 85 bookmarkItems: Record<string, BookmarkMediaItem>, 81 86 groupOrderItems: string[], 82 87 ) => { 83 88 if ( 84 89 Object.keys(progressItems).length === 0 && 90 + Object.keys(watchHistoryItems).length === 0 && 85 91 Object.keys(bookmarkItems).length === 0 && 86 92 groupOrderItems.length === 0 87 93 ) { ··· 92 98 ([tmdbId, item]) => progressMediaItemToInputs(tmdbId, item), 93 99 ); 94 100 101 + const watchHistoryInputs = watchHistoryItemsToInputs(watchHistoryItems); 102 + 95 103 const bookmarkInputs = Object.entries(bookmarkItems).map( 96 104 ([tmdbId, item]) => bookmarkMediaToInput(tmdbId, item), 97 105 ); 98 106 99 107 const importPromises = [ 100 108 importProgress(backendUrlInner, account, progressInputs), 109 + importWatchHistory(backendUrlInner, account, watchHistoryInputs), 101 110 importBookmarks(backendUrlInner, account, bookmarkInputs), 102 111 ]; 103 112 ··· 177 186 bytesToBase64(keys.seed), 178 187 ); 179 188 180 - await importData(backendUrl, account, progress, bookmarks, groupOrder); 189 + await importData(backendUrl, account, progress, watchHistory, bookmarks, groupOrder); 181 190 182 191 return account; 183 192 }, ··· 186 195 userDataLogin, 187 196 bookmarks, 188 197 progress, 198 + watchHistory, 189 199 groupOrder, 190 200 preferences, 191 201 subtitleLanguage,
+20 -2
src/pages/migration/MigrationDownload.tsx
··· 16 16 import { useGroupOrderStore } from "@/stores/groupOrder"; 17 17 import { useProgressStore } from "@/stores/progress"; 18 18 import { useSubtitleStore } from "@/stores/subtitles"; 19 + import { useWatchHistoryStore } from "@/stores/watchHistory"; 19 20 20 21 export function MigrationDownloadPage() { 21 22 const { t } = useTranslation(); ··· 23 24 const navigate = useNavigate(); 24 25 const bookmarks = useBookmarkStore((s) => s.bookmarks); 25 26 const progress = useProgressStore((s) => s.items); 27 + const watchHistory = useWatchHistoryStore((s) => s.items); 26 28 const groupOrder = useGroupOrderStore((s) => s.groupOrder); 27 29 28 30 // Get data from localStorage directly to ensure we have the persisted data ··· 37 39 38 40 const persistedBookmarks = getPersistedData("__MW::bookmarks"); 39 41 const persistedProgress = getPersistedData("__MW::progress"); 42 + const persistedWatchHistory = getPersistedData("__MW::watchHistory"); 40 43 const persistedGroupOrder = getPersistedData("__MW::groupOrder"); 41 44 const persistedPreferences = getPersistedData("__MW::preferences"); 42 45 const persistedSubtitles = getPersistedData("__MW::subtitles"); ··· 55 58 }, 56 59 bookmarks: persistedBookmarks.bookmarks || bookmarks, 57 60 progress: persistedProgress.items || progress, 61 + watchHistory: persistedWatchHistory.items || watchHistory, 58 62 groupOrder: persistedGroupOrder.groupOrder || groupOrder, 59 63 settings: { 60 64 ...persistedPreferences, ··· 104 108 console.error("Error during data download:", error); 105 109 setStatus("error"); 106 110 } 107 - }, [ 111 + }, [ 108 112 bookmarks, 109 113 progress, 114 + watchHistory, 110 115 user.account, 111 116 groupOrder, 112 117 persistedBookmarks, 113 118 persistedProgress, 119 + persistedWatchHistory, 114 120 persistedGroupOrder, 115 121 persistedPreferences, 116 122 persistedSubtitles, ··· 138 144 <h3 className="font-bold text-white text-lg"> 139 145 {t("migration.preview.downloadDescription")} 140 146 </h3> 141 - <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> 147 + <div className="grid grid-cols-1 md:grid-cols-4 gap-4"> 142 148 <div className="p-4 bg-background rounded-lg"> 143 149 <div className="flex items-center gap-2"> 144 150 <Icon icon={Icons.CLOCK} className="text-xl" /> ··· 163 169 Object.keys(persistedBookmarks.bookmarks || bookmarks) 164 170 .length 165 171 } 172 + </div> 173 + </div> 174 + 175 + <div className="p-4 bg-background rounded-lg"> 176 + <div className="flex items-center gap-2"> 177 + <Icon icon={Icons.HISTORY} className="text-xl" /> 178 + <span className="font-medium"> 179 + {t("migration.preview.items.watchHistory")} 180 + </span> 181 + </div> 182 + <div className="text-xl font-bold mt-2"> 183 + {Object.keys(persistedWatchHistory.items || watchHistory).length} 166 184 </div> 167 185 </div> 168 186
+73 -1
src/pages/migration/MigrationUpload.tsx
··· 8 8 importGroupOrder, 9 9 importProgress, 10 10 importSettings, 11 + importWatchHistory, 11 12 } from "@/backend/accounts/import"; 13 + import { watchHistoryItemsToInputs } from "@/backend/accounts/watchHistory"; 12 14 import { progressMediaItemToInputs } from "@/backend/accounts/progress"; 13 15 import { Button } from "@/components/buttons/Button"; 14 16 import { Icon, Icons } from "@/components/Icon"; ··· 28 30 import { ProgressMediaItem, useProgressStore } from "@/stores/progress"; 29 31 import { useSubtitleStore } from "@/stores/subtitles"; 30 32 import { useThemeStore } from "@/stores/theme"; 33 + import { WatchHistoryItem, useWatchHistoryStore } from "@/stores/watchHistory"; 31 34 32 35 interface UploadedData { 33 36 account?: { ··· 40 43 }; 41 44 bookmarks?: Record<string, BookmarkMediaItem>; 42 45 progress?: Record<string, ProgressMediaItem>; 46 + watchHistory?: Record<string, WatchHistoryItem>; 43 47 groupOrder?: string[]; 44 48 settings?: any; 45 49 theme?: string | null; ··· 55 59 const fileInputRef = useRef<HTMLInputElement>(null); 56 60 const replaceBookmarks = useBookmarkStore((s) => s.replaceBookmarks); 57 61 const replaceProgress = useProgressStore((s) => s.replaceItems); 62 + const replaceWatchHistory = useWatchHistoryStore((s) => s.replaceItems); 58 63 const setGroupOrder = useGroupOrderStore((s) => s.setGroupOrder); 59 64 const preferencesStore = usePreferencesStore(); 60 65 const subtitleStore = useSubtitleStore(); ··· 133 138 {} as Record<string, ProgressMediaItem>, 134 139 ) 135 140 : undefined, 141 + 142 + watchHistory: parsedData.watchHistory 143 + ? Object.entries(parsedData.watchHistory).reduce( 144 + (acc, [id, item]: [string, any]) => { 145 + // Ensure type is either "show" or "movie" 146 + if ( 147 + typeof item.type === "string" && 148 + (item.type === "show" || item.type === "movie") 149 + ) { 150 + acc[id] = { 151 + title: item.title || "", 152 + poster: item.poster, 153 + type: item.type as "show" | "movie", 154 + year: typeof item.year === "number" ? item.year : undefined, 155 + progress: item.progress, 156 + watchedAt: 157 + typeof item.watchedAt === "number" 158 + ? item.watchedAt 159 + : Date.now(), 160 + completed: typeof item.completed === "boolean" ? item.completed : false, 161 + episodeId: item.episodeId, 162 + seasonId: item.seasonId, 163 + seasonNumber: item.seasonNumber, 164 + episodeNumber: item.episodeNumber, 165 + }; 166 + } 167 + return acc; 168 + }, 169 + {} as Record<string, WatchHistoryItem>, 170 + ) 171 + : undefined, 136 172 }; 137 173 138 174 setUploadedData(validatedData); ··· 173 209 ); 174 210 } 175 211 212 + // Import watch history 213 + if ( 214 + uploadedData.watchHistory && 215 + Object.keys(uploadedData.watchHistory).length > 0 216 + ) { 217 + const watchHistoryInputs = watchHistoryItemsToInputs(uploadedData.watchHistory); 218 + importPromises.push( 219 + importWatchHistory(backendUrl, user.account, watchHistoryInputs), 220 + ); 221 + } 222 + 176 223 // Import bookmarks 177 224 if ( 178 225 uploadedData.bookmarks && ··· 234 281 replaceProgress(uploadedData.progress); 235 282 } 236 283 284 + if (uploadedData.watchHistory) { 285 + replaceWatchHistory(uploadedData.watchHistory); 286 + } 287 + 237 288 // Import all data types to backend 238 289 try { 239 290 await handleBackendImport(); ··· 268 319 JSON.stringify({ state: { items: uploadedData.progress } }), 269 320 ); 270 321 replaceProgress(uploadedData.progress); 322 + } 323 + if (uploadedData.watchHistory) { 324 + localStorage.setItem( 325 + "__MW::watchHistory", 326 + JSON.stringify({ state: { items: uploadedData.watchHistory } }), 327 + ); 328 + replaceWatchHistory(uploadedData.watchHistory); 271 329 } 272 330 if (uploadedData.groupOrder) { 273 331 localStorage.setItem( ··· 550 608 </Heading2> 551 609 <Divider marginClass="my-6 px-8 box-content -mx-8" /> 552 610 553 - <div className="grid grid-cols-2 gap-4"> 611 + <div className="grid grid-cols-3 gap-4"> 554 612 <div className="p-4 bg-background rounded-lg"> 555 613 <div className="flex items-center gap-2"> 556 614 <Icon icon={Icons.CLOCK} className="text-xl" /> ··· 575 633 <div className="text-xl font-bold mt-2"> 576 634 {uploadedData.bookmarks 577 635 ? Object.keys(uploadedData.bookmarks).length 636 + : 0} 637 + </div> 638 + </div> 639 + 640 + <div className="p-4 bg-background rounded-lg"> 641 + <div className="flex items-center gap-2"> 642 + <Icon icon={Icons.HISTORY} className="text-xl" /> 643 + <span className="font-medium"> 644 + {t("migration.preview.items.watchHistory")} 645 + </span> 646 + </div> 647 + <div className="text-xl font-bold mt-2"> 648 + {uploadedData.watchHistory 649 + ? Object.keys(uploadedData.watchHistory).length 578 650 : 0} 579 651 </div> 580 652 </div>
+15 -7
src/stores/progress/index.ts
··· 141 141 duration: 0, 142 142 watched: 0, 143 143 }; 144 + 145 + const wasCompleted = item.progress.duration > 0 && item.progress.watched / item.progress.duration > 0.9; 144 146 item.progress = { ...progress }; 145 147 146 - // Update watch history 147 - const completed = 148 + // Update watch history only if becoming completed 149 + const isCompleted = 148 150 progress.duration > 0 && 149 151 progress.watched / progress.duration > 0.9; 150 - useWatchHistoryStore.getState().addItem(meta, progress, completed); 152 + if (isCompleted && !wasCompleted) { 153 + useWatchHistoryStore.getState().addItem(meta, progress, true); 154 + } 151 155 return; 152 156 } 153 157 ··· 173 177 }, 174 178 }; 175 179 176 - item.episodes[meta.episode.tmdbId].progress = { ...progress }; 180 + const episodeItem = item.episodes[meta.episode.tmdbId]; 181 + const wasCompleted = episodeItem.progress.duration > 0 && episodeItem.progress.watched / episodeItem.progress.duration > 0.9; 182 + episodeItem.progress = { ...progress }; 177 183 178 - // Update watch history 179 - const completed = 184 + // Update watch history only if becoming completed 185 + const isCompleted = 180 186 progress.duration > 0 && progress.watched / progress.duration > 0.9; 181 - useWatchHistoryStore.getState().addItem(meta, progress, completed); 187 + if (isCompleted && !wasCompleted) { 188 + useWatchHistoryStore.getState().addItem(meta, progress, true); 189 + } 182 190 }); 183 191 }, 184 192 clear() {
+29 -17
src/stores/watchHistory/index.ts
··· 70 70 updateQueue: [], 71 71 addItem(meta, progress, completed) { 72 72 set((s) => { 73 + const key = meta.episode 74 + ? `${meta.tmdbId}-${meta.episode.tmdbId}` 75 + : meta.tmdbId; 76 + 77 + // Only add/update if this is a completion or if the item doesn't exist yet 78 + const existingItem = s.items[key]; 79 + const shouldUpdate = !existingItem || (completed && !existingItem.completed); 80 + 81 + if (!shouldUpdate) return; 82 + 73 83 // add to updateQueue 74 84 updateId += 1; 75 85 s.updateQueue.push({ ··· 90 100 }); 91 101 92 102 // add to watch history store 93 - const key = meta.episode 94 - ? `${meta.tmdbId}-${meta.episode.tmdbId}` 95 - : meta.tmdbId; 96 103 s.items[key] = { 97 104 type: meta.type, 98 105 title: meta.title, ··· 110 117 }, 111 118 updateItem(id, progress, completed) { 112 119 set((s) => { 113 - if (!s.items[id]) return; 120 + const existingItem = s.items[id]; 121 + if (!existingItem) return; 122 + 123 + // Only update if this is becoming completed and wasn't completed before 124 + const shouldUpdate = completed && !existingItem.completed; 125 + 126 + if (!shouldUpdate) return; 114 127 115 128 // add to updateQueue 116 129 updateId += 1; 117 - const item = s.items[id]; 118 130 s.updateQueue.push({ 119 - tmdbId: item.episodeId ? item.seasonId || id.split("-")[0] : id, 120 - title: item.title, 121 - year: item.year, 122 - poster: item.poster, 123 - type: item.type, 131 + tmdbId: existingItem.episodeId ? existingItem.seasonId || id.split("-")[0] : id, 132 + title: existingItem.title, 133 + year: existingItem.year, 134 + poster: existingItem.poster, 135 + type: existingItem.type, 124 136 progress: { ...progress }, 125 137 watchedAt: Date.now(), 126 138 completed, 127 139 id: updateId.toString(), 128 - episodeId: item.episodeId, 129 - seasonId: item.seasonId, 130 - seasonNumber: item.seasonNumber, 131 - episodeNumber: item.episodeNumber, 140 + episodeId: existingItem.episodeId, 141 + seasonId: existingItem.seasonId, 142 + seasonNumber: existingItem.seasonNumber, 143 + episodeNumber: existingItem.episodeNumber, 132 144 action: "update", 133 145 }); 134 146 135 147 // update item 136 - item.progress = { ...progress }; 137 - item.watchedAt = Date.now(); 138 - item.completed = completed; 148 + existingItem.progress = { ...progress }; 149 + existingItem.watchedAt = Date.now(); 150 + existingItem.completed = completed; 139 151 }); 140 152 }, 141 153 removeItem(id) {