diff --git a/.env.sample b/.env.sample index 1971f20..a47d796 100644 --- a/.env.sample +++ b/.env.sample @@ -15,4 +15,5 @@ GEMINI_API_KEY="" # Gemini API Key for parsing search query in "Find" # Immich Share Link IMMICH_SHARE_LINK_KEY="" # Share link key for Immich -POWER_TOOLS_ENDPOINT_URL="" # URL of the Power Tools endpoint (Used for share links) \ No newline at end of file +POWER_TOOLS_ENDPOINT_URL="" # URL of the Power Tools endpoint (Used for share links) +JWT_SECRET="" # JWT Secret for authentication diff --git a/bun.lockb b/bun.lockb index 741a0e0..0b94ea6 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/src/components/albums/info/AlbumPeople.tsx b/src/components/albums/info/AlbumPeople.tsx index cd4dc33..5c6f03f 100644 --- a/src/components/albums/info/AlbumPeople.tsx +++ b/src/components/albums/info/AlbumPeople.tsx @@ -17,6 +17,7 @@ import { PersonMergeDropdown } from '@/components/people/PersonMergeDropdown' import { Button } from '@/components/ui/button' import { updatePerson } from '@/handlers/api/people.handler' import { Checkbox } from '@/components/ui/checkbox' +import { Input } from '@/components/ui/input' interface AlbumPeopleProps { album: IAlbum @@ -40,11 +41,16 @@ export default function AlbumPeople({ album, onSelect, readOnly }: AlbumPeoplePr const [hidingPerson, setHidingPerson] = useState(false) const [selectionMode, setSelectionMode] = useState(false) const [selectedPeople, setSelectedPeople] = useState([]) + const [searchQuery, setSearchQuery] = useState("") const selectedPerson = useMemo(() => { return people.find((person) => query.faceId === person.id) }, [people, query.faceId]) + const filteredPeople = useMemo(() => { + return people.filter((person) => person.name.toLowerCase().includes(searchQuery.toLowerCase())) + }, [people, searchQuery]) + const fetchPeople = async () => { return getAlbumPeople(album.id).then((people) => { setPeople(people) @@ -102,7 +108,12 @@ export default function AlbumPeople({ album, onSelect, readOnly }: AlbumPeoplePr return (
- + setSearchQuery(e.target.value)} + /> {selectedPerson && (
@@ -172,7 +183,7 @@ export default function AlbumPeople({ album, onSelect, readOnly }: AlbumPeoplePr )}
- {people.map((person) => ( + {filteredPeople.map((person) => (
("") + + const filteredPeople = useMemo(() => { + return people.filter((person) => person.name.toLowerCase().includes(searchQuery.toLowerCase())) + }, [people, searchQuery]) + const isSelected = useCallback((person: IPerson) => { return selectedIds?.includes(person.id) }, [selectedIds]) return ( -
- {people.map((person) => ( +
+ setSearchQuery(e.target.value)} + /> + {filteredPeople.map((person) => (
BASE_API_ENDPOINT + "/share-link/" + token; export const SHARE_LINK_GENERATE_PATH = BASE_API_ENDPOINT + "/share-link/generate"; export const SHARE_LINK_ASSETS_PATH = (token: string) => BASE_API_ENDPOINT + "/share-link/" + token + "/assets"; -export const SHARE_LINK_PEOPLE_PATH = (token: string) => BASE_API_ENDPOINT + "/share-link/" + token + "/people"; \ No newline at end of file +export const SHARE_LINK_PEOPLE_PATH = (token: string) => BASE_API_ENDPOINT + "/share-link/" + token + "/people"; +export const SHARE_LINK_DOWNLOAD_PATH = (token: string) => BASE_API_ENDPOINT + "/share-link/" + token + "/download"; +export const DOWNLOAD_ASSETS_PATH = BASE_PROXY_ENDPOINT + "/download/archive"; \ No newline at end of file diff --git a/src/handlers/api/shareLink.handler.ts b/src/handlers/api/shareLink.handler.ts index a6886a9..b6616a4 100644 --- a/src/handlers/api/shareLink.handler.ts +++ b/src/handlers/api/shareLink.handler.ts @@ -1,6 +1,7 @@ import { SHARE_LINK_ASSETS_PATH, + SHARE_LINK_DOWNLOAD_PATH, SHARE_LINK_GENERATE_PATH, SHARE_LINK_PATH, SHARE_LINK_PEOPLE_PATH @@ -22,4 +23,8 @@ export const generateShareLink = async (filters: ShareLinkFilters) => { export const getShareLinkPeople = async (token: string) => { return API.get(SHARE_LINK_PEOPLE_PATH(token)); +} + +export const downloadShareLink = async (token: string, assetIds: string[]) => { + return API.post(SHARE_LINK_DOWNLOAD_PATH(token), { assetIds }); } \ No newline at end of file diff --git a/src/pages/api/share-link/[token]/download.ts b/src/pages/api/share-link/[token]/download.ts new file mode 100644 index 0000000..90f09ed --- /dev/null +++ b/src/pages/api/share-link/[token]/download.ts @@ -0,0 +1,70 @@ +import { db } from "@/config/db"; +import { ENV } from "@/config/environment"; +import { count, desc, gte, isNotNull, lte, ne } from "drizzle-orm"; +import { eq, inArray } from "drizzle-orm"; +import { assetFaces, assets, exif, person } from "@/schema"; +import { and } from "drizzle-orm"; +import { JsonWebTokenError, sign, verify } from "jsonwebtoken"; +import { NextApiResponse } from "next"; + +import { NextApiRequest } from "next"; +import { albums } from "@/schema/albums.schema"; +import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema"; +import { ASSET_SHARE_THUMBNAIL_PATH, DOWNLOAD_ASSETS_PATH, SHARE_LINK_DOWNLOAD_PATH } from "@/config/routes"; +import { getUserHeaders } from "@/helpers/user.helper"; + +interface ShareLinkFilters { + personIds: string[]; + albumIds: string[]; + startDate: string; + endDate: string; +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { token } = req.query; + const { assetIds } = req.body; + try { + if (!token) { + return res.status(404).json({ message: "Token not found" }); + } + if (req.method !== "POST") { + return res.status(405).json({ message: "Method not allowed" }); + } + + const decoded = verify(token as string, ENV.JWT_SECRET); + + if (!decoded) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const targetUrl = `${ENV.IMMICH_URL}/api/download/archive`; + + const response = await fetch(targetUrl, { + method: 'POST', + body: JSON.stringify({ assetIds }), + headers: getUserHeaders({ isUsingShareKey: true }, { + 'Content-Type': 'application/json', + }), + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + // Get the image data from the response + const imageBuffer = await response.arrayBuffer() + + // Set the appropriate headers for the image response + res.setHeader('Content-Type', response.headers.get('Content-Type') || 'image/png') + res.setHeader('Content-Length', imageBuffer.byteLength) + + // Send the image data + res.send(Buffer.from(imageBuffer)) + + } catch (error) { + if (error instanceof JsonWebTokenError) { + return res.status(401).json({ message: "Please check your link and try again. Looks like it's expired." }); + } + return res.status(500).json({ message: (error as Error).message }); + } +} \ No newline at end of file