fix: migrate to Immich v3.0 API and DB schema

- Updated albumColumns and AlbumThumbnail components to display placeholder when no thumbnail is available.
- added album_user table (album ownership moved here, role='owner')
- drop asset.deviceId/deviceAssetId and albums.ownerId
- asset.duration varchar -> integer (milliseconds)
- removed isArchived instead use visibility
This commit is contained in:
sai80082 committed 2026-07-02 20:13:29 +05:30
1 parent 303abc72cd
commit af4cfceb47
20 files changed
+110 -79

No files matched your search

+25 -15
View File
@@ -2,7 +2,7 @@ import { ColumnDef } from "@tanstack/react-table"
import { IAlbum } from "@/types/album" import { IAlbum } from "@/types/album"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { ArrowUpDown, MoreHorizontal, Calendar, Camera, Users, HardDrive } from "lucide-react" import { ArrowUpDown, MoreHorizontal, Calendar, Camera, Users, HardDrive, ImageOff } from "lucide-react"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -48,15 +48,21 @@ export const albumColumns: ColumnDef<IAlbum>[] = [
const album = row.original const album = row.original
return ( return (
<div className="w-16 h-16 rounded-md overflow-hidden"> <div className="w-16 h-16 rounded-md overflow-hidden">
<LazyImage {album.albumThumbnailAssetId ? (
src={ASSET_THUMBNAIL_PATH(album.albumThumbnailAssetId)} <LazyImage
alt={album.albumName} src={ASSET_THUMBNAIL_PATH(album.albumThumbnailAssetId)}
style={{ alt={album.albumName}
width: '100%', style={{
height: '100%', width: '100%',
objectFit: 'cover', height: '100%',
}} objectFit: 'cover',
/> }}
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-gray-100 dark:bg-gray-800 text-gray-400">
<ImageOff size={20} />
</div>
)}
</div> </div>
) )
}, },
@@ -164,16 +170,20 @@ export const albumColumns: ColumnDef<IAlbum>[] = [
}, },
cell: ({ row }) => { cell: ({ row }) => {
const album = row.original const album = row.original
const numberOfDays = differenceInDays(album.lastPhotoDate, album.firstPhotoDate) const numberOfDays = (album.firstPhotoDate && album.lastPhotoDate)
? differenceInDays(album.lastPhotoDate, album.firstPhotoDate)
: null
return ( return (
<div className="space-y-1"> <div className="space-y-1">
<div className="text-sm"> <div className="text-sm">
{album.firstPhotoDate ? formatDate(album.firstPhotoDate.toString(), 'MMM d, yyyy') : ''} - {album.lastPhotoDate ? formatDate(album.lastPhotoDate.toString(), 'MMM d, yyyy') : ''} {album.firstPhotoDate ? formatDate(album.firstPhotoDate.toString(), 'MMM d, yyyy') : ''} - {album.lastPhotoDate ? formatDate(album.lastPhotoDate.toString(), 'MMM d, yyyy') : ''}
</div> </div>
<div className="text-xs text-muted-foreground"> {numberOfDays !== null && (
{numberOfDays === 0 ? '1 day' : `${numberOfDays.toLocaleString()} days`} <div className="text-xs text-muted-foreground">
</div> {numberOfDays === 0 ? '1 day' : `${numberOfDays.toLocaleString()} days`}
</div>
)}
</div> </div>
) )
}, },
+30 -14
View File
@@ -8,7 +8,7 @@ import { IAlbum } from '@/types/album';
import { formatDate } from '@/helpers/date.helper'; import { formatDate } from '@/helpers/date.helper';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
import { Calendar, Camera } from 'lucide-react'; import { Calendar, Camera, ImageOff } from 'lucide-react';
interface IAlbumThumbnailProps { interface IAlbumThumbnailProps {
album: IAlbum; album: IAlbum;
@@ -18,6 +18,9 @@ interface IAlbumThumbnailProps {
export default function AlbumThumbnail({ album, onSelect, selected }: IAlbumThumbnailProps) { export default function AlbumThumbnail({ album, onSelect, selected }: IAlbumThumbnailProps) {
const numberOfDays = useMemo(() => { const numberOfDays = useMemo(() => {
if (!album.firstPhotoDate || !album.lastPhotoDate) {
return null;
}
return differenceInDays(album.lastPhotoDate, album.firstPhotoDate); return differenceInDays(album.lastPhotoDate, album.firstPhotoDate);
}, [album.firstPhotoDate, album.lastPhotoDate]); }, [album.firstPhotoDate, album.lastPhotoDate]);
@@ -29,16 +32,27 @@ export default function AlbumThumbnail({ album, onSelect, selected }: IAlbumThum
return ( return (
<div className="border rounded-lg overflow-hidden shadow-lg relative group"> <div className="border rounded-lg overflow-hidden shadow-lg relative group">
<label className="block relative "> <label className="block relative ">
<LazyImage {album.albumThumbnailAssetId ? (
src={ASSET_THUMBNAIL_PATH(album.albumThumbnailAssetId)} <LazyImage
alt={album.albumName} src={ASSET_THUMBNAIL_PATH(album.albumThumbnailAssetId)}
title={album.albumName} alt={album.albumName}
style={{ title={album.albumName}
width: '100%', style={{
height: '200px', width: '100%',
objectFit: 'cover', height: '200px',
}} objectFit: 'cover',
/> }}
/>
) : (
<div
className="flex flex-col items-center justify-center gap-2 bg-gray-100 dark:bg-gray-800 text-gray-400"
style={{ width: '100%', height: '200px' }}
title={album.albumName}
>
<ImageOff size={32} />
<span className="text-xs">No photos</span>
</div>
)}
<div className="absolute bottom-0 w-full bg-gray-800/70 text-white text-center text-xs font-bold py-1 group-hover:hidden"> <div className="absolute bottom-0 w-full bg-gray-800/70 text-white text-center text-xs font-bold py-1 group-hover:hidden">
{album.firstPhotoDate ? formatDate(album.firstPhotoDate?.toString(), 'MMM d, yyyy') : ''} - {album.lastPhotoDate ? formatDate(album.lastPhotoDate?.toString(), 'MMM d, yyyy') : ''} {album.firstPhotoDate ? formatDate(album.firstPhotoDate?.toString(), 'MMM d, yyyy') : ''} - {album.lastPhotoDate ? formatDate(album.lastPhotoDate?.toString(), 'MMM d, yyyy') : ''}
</div> </div>
@@ -54,9 +68,11 @@ export default function AlbumThumbnail({ album, onSelect, selected }: IAlbumThum
<Camera size={12} /> <Camera size={12} />
{humanizeNumber(album.assetCount)} {humanizeNumber(album.assetCount)}
</div> </div>
<p className="bg-gray-800 flex items-center gap-1 text-white text-xs py-1 px-2 rounded"> {numberOfDays !== null && (
{numberOfDays === 0 ? '1 day' : `${numberOfDays.toLocaleString() } days`} <Calendar size={12} /> <p className="bg-gray-800 flex items-center gap-1 text-white text-xs py-1 px-2 rounded">
</p> {numberOfDays === 0 ? '1 day' : `${numberOfDays.toLocaleString() } days`} <Calendar size={12} />
</p>
)}
</div> </div>
<div className={`p-4 ${selected ? 'bg-blue-500' : ''}`}> <div className={`p-4 ${selected ? 'bg-blue-500' : ''}`}>
<Link href={`/albums/${album.id}`}> <Link href={`/albums/${album.id}`}>
+14 -10
View File
@@ -7,6 +7,9 @@ export const pluralize = (number: number, word: string, pluralWord: string) => {
} }
export const humanizeBytes = (bytes: number) => { export const humanizeBytes = (bytes: number) => {
if (!bytes || isNaN(bytes)) {
return `0 bytes`
}
if (bytes < 1024) { if (bytes < 1024) {
return `${bytes} bytes` return `${bytes} bytes`
} }
@@ -19,22 +22,23 @@ export const humanizeBytes = (bytes: number) => {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB` return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
} }
export const humanizeDuration = (duration: string) => { export const humanizeDuration = (duration: string | number | null | undefined) => {
if (!duration) { if (duration === null || duration === undefined || duration === '') {
return null; return null;
} }
// Example input : 00:00:04.350 const ms = Number(duration);
const [hours, minutes, seconds] = duration.split(':').map(Number); if (isNaN(ms)) {
const totalSeconds = Math.round(hours * 3600 + minutes * 60 + seconds); return null;
}
const totalSeconds = Math.round(ms / 1000);
if (totalSeconds < 60) { if (totalSeconds < 60) {
return `${totalSeconds}s`; return `${totalSeconds}s`;
} }
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
if (totalSeconds < 3600) { if (totalSeconds < 3600) {
// Show only minutes return `${minutes}m`;
return `${Math.round(minutes)}m`;
} }
return `${hours}h ${minutes}m`;
return `${Math.round(hours)}h ${Math.round(minutes)}m`;
} }
+2 -3
View File
@@ -163,16 +163,15 @@ export async function executeAction(
} }
case "archive": { case "archive": {
await immichFetch("/assets", "PUT", { ids: assetIds, isArchived: true }, user); await immichFetch("/assets", "PUT", { ids: assetIds, visibility: "archive" }, user);
return { action: "archive", assetsProcessed: assetIds.length }; return { action: "archive", assetsProcessed: assetIds.length };
} }
case "tag": { case "tag": {
if (!config.tagName) throw new Error("Tag name is required"); if (!config.tagName) throw new Error("Tag name is required");
// Tags API may vary by Immich version — try the standard approach
try { try {
// Create or get tag // Create or get tag
const tag = await immichFetch("/tags", "POST", { name: config.tagName, type: "OBJECT" }, user); const tag = await immichFetch("/tags", "POST", { name: config.tagName }, user);
// Tag assets // Tag assets
await immichFetch(`/tags/${tag.id}/assets`, "PUT", { ids: assetIds }, user); await immichFetch(`/tags/${tag.id}/assets`, "PUT", { ids: assetIds }, user);
return { action: "tag", assetsProcessed: assetIds.length }; return { action: "tag", assetsProcessed: assetIds.length };
-1
View File
@@ -29,7 +29,6 @@ export default async function handler(
} }
const dbAssets = await db.selectDistinctOn([assets.id], { const dbAssets = await db.selectDistinctOn([assets.id], {
id: assets.id, id: assets.id,
deviceId: assets.deviceId,
type: assets.type, type: assets.type,
originalPath: assets.originalPath, originalPath: assets.originalPath,
isFavorite: assets.isFavorite, isFavorite: assets.isFavorite,
+3 -1
View File
@@ -7,6 +7,7 @@ import { albums } from "@/schema/albums.schema";
import { count, eq, min, max, sql, and } from "drizzle-orm"; import { count, eq, min, max, sql, and } from "drizzle-orm";
import { assets } from "@/schema/assets.schema"; import { assets } from "@/schema/assets.schema";
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema"; import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
import { albumUsers } from "@/schema/albumUsers.schema";
import { assetFaces, exif, person } from "@/schema"; import { assetFaces, exif, person } from "@/schema";
export default async function handler( export default async function handler(
@@ -31,12 +32,13 @@ export default async function handler(
faceCount: count(sql<string>`DISTINCT ${person.id}`), // Ensure unique personId faceCount: count(sql<string>`DISTINCT ${person.id}`), // Ensure unique personId
}) })
.from(albums) .from(albums)
.innerJoin(albumUsers, and(eq(albums.id, albumUsers.albumId), eq(albumUsers.role, "owner")))
.leftJoin(albumsAssetsAssets, eq(albums.id, albumsAssetsAssets.albumId)) .leftJoin(albumsAssetsAssets, eq(albums.id, albumsAssetsAssets.albumId))
.leftJoin(assets, eq(albumsAssetsAssets.assetId, assets.id)) .leftJoin(assets, eq(albumsAssetsAssets.assetId, assets.id))
.leftJoin(exif, and(eq(assets.id, exif.assetId), eq(assets.visibility, "timeline"))) .leftJoin(exif, and(eq(assets.id, exif.assetId), eq(assets.visibility, "timeline")))
.leftJoin(assetFaces, eq(assets.id, assetFaces.assetId)) .leftJoin(assetFaces, eq(assets.id, assetFaces.assetId))
.leftJoin(person, and(eq(assetFaces.personId, person.id), eq(person.isHidden, false))) .leftJoin(person, and(eq(assetFaces.personId, person.id), eq(person.isHidden, false)))
.where(and(eq(albums.ownerId, currentUser.id), eq(albums.id, id))) .where(and(eq(albumUsers.userId, currentUser.id), eq(albums.id, id)))
.groupBy(albums.id) .groupBy(albums.id)
.limit(1); .limit(1);
+3 -1
View File
@@ -7,6 +7,7 @@ import { albums } from "@/schema/albums.schema";
import { count, desc, eq, and, isNotNull } from "drizzle-orm"; import { count, desc, eq, and, isNotNull } from "drizzle-orm";
import { assets } from "@/schema/assets.schema"; import { assets } from "@/schema/assets.schema";
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema"; import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
import { albumUsers } from "@/schema/albumUsers.schema";
import { assetFaces, person } from "@/schema"; import { assetFaces, person } from "@/schema";
export default async function handler( export default async function handler(
@@ -26,11 +27,12 @@ export default async function handler(
numberOfPhotos: count(assets.id), numberOfPhotos: count(assets.id),
}) })
.from(albums) .from(albums)
.innerJoin(albumUsers, and(eq(albums.id, albumUsers.albumId), eq(albumUsers.role, "owner")))
.leftJoin(albumsAssetsAssets, eq(albums.id, albumsAssetsAssets.albumId)) .leftJoin(albumsAssetsAssets, eq(albums.id, albumsAssetsAssets.albumId))
.leftJoin(assets, eq(albumsAssetsAssets.assetId, assets.id)) .leftJoin(assets, eq(albumsAssetsAssets.assetId, assets.id))
.leftJoin(assetFaces, eq(assets.id, assetFaces.assetId)) .leftJoin(assetFaces, eq(assets.id, assetFaces.assetId))
.leftJoin(person, and(eq(assetFaces.personId, person.id), eq(person.isHidden, false))) .leftJoin(person, and(eq(assetFaces.personId, person.id), eq(person.isHidden, false)))
.where(and(eq(albums.ownerId, currentUser.id), eq(albums.id, id), isNotNull(person.id))) .where(and(eq(albumUsers.userId, currentUser.id), eq(albums.id, id), isNotNull(person.id)))
.orderBy(desc(person.name)) .orderBy(desc(person.name))
.groupBy(person.id); .groupBy(person.id);
+5 -3
View File
@@ -7,6 +7,7 @@ import { albums } from "@/schema/albums.schema";
import { count, desc, eq, min, max, sql, and, sum, isNotNull } from "drizzle-orm"; import { count, desc, eq, min, max, sql, and, sum, isNotNull } from "drizzle-orm";
import { assets } from "@/schema/assets.schema"; import { assets } from "@/schema/assets.schema";
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema"; import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
import { albumUsers } from "@/schema/albumUsers.schema";
import { assetFaces, exif, person } from "@/schema"; import { assetFaces, exif, person } from "@/schema";
import { IAlbum } from "@/types/album"; import { IAlbum } from "@/types/album";
@@ -63,12 +64,13 @@ export default async function handler(
size: sum(exif.fileSizeInByte), size: sum(exif.fileSizeInByte),
order: albums.order, order: albums.order,
isActivityEnabled: albums.isActivityEnabled, isActivityEnabled: albums.isActivityEnabled,
ownerId: albums.ownerId, ownerId: albumUsers.userId,
description: albums.description, description: albums.description,
lastModifiedAssetTimestamp: max(exif.dateTimeOriginal), lastModifiedAssetTimestamp: max(exif.dateTimeOriginal),
faceCount: count(sql<string>`DISTINCT ${assetFaces.personId}`), // Ensure unique personId faceCount: count(sql<string>`DISTINCT ${assetFaces.personId}`), // Ensure unique personId
}) })
.from(albums) .from(albums)
.innerJoin(albumUsers, and(eq(albums.id, albumUsers.albumId), eq(albumUsers.role, "owner")))
.leftJoin(albumsAssetsAssets, eq(albums.id, albumsAssetsAssets.albumId)) .leftJoin(albumsAssetsAssets, eq(albums.id, albumsAssetsAssets.albumId))
.leftJoin(assets, and( .leftJoin(assets, and(
eq(albumsAssetsAssets.assetId, assets.id), eq(albumsAssetsAssets.assetId, assets.id),
@@ -78,8 +80,8 @@ export default async function handler(
.leftJoin(exif, eq(assets.id, exif.assetId)) .leftJoin(exif, eq(assets.id, exif.assetId))
.leftJoin(assetFaces, eq(assets.id, assetFaces.assetId)) .leftJoin(assetFaces, eq(assets.id, assetFaces.assetId))
.leftJoin(person, and(eq(assetFaces.personId, person.id), eq(person.isHidden, false))) .leftJoin(person, and(eq(assetFaces.personId, person.id), eq(person.isHidden, false)))
.where(eq(albums.ownerId, currentUser.id)) .where(eq(albumUsers.userId, currentUser.id))
.groupBy(albums.id) .groupBy(albums.id, albumUsers.userId)
.orderBy(desc(albums.createdAt)); .orderBy(desc(albums.createdAt));
const sortedAlbums = sortAlbums(dbAlbums as IAlbum[], sortBy, sortOrder); const sortedAlbums = sortAlbums(dbAlbums as IAlbum[], sortBy, sortOrder);
+8 -2
View File
@@ -5,6 +5,7 @@ import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import API from "@/lib/api"; import API from "@/lib/api";
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema"; import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
import { albums } from "@/schema/albums.schema"; import { albums } from "@/schema/albums.schema";
import { albumUsers } from "@/schema/albumUsers.schema";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
@@ -35,11 +36,16 @@ export default async function handler(
}); });
} }
const ownedAlbumIds = db
.select({ albumId: albumUsers.albumId })
.from(albumUsers)
.where(and(eq(albumUsers.userId, currentUser.id), eq(albumUsers.role, "owner")));
const primaryAlbum = await db const primaryAlbum = await db
.select() .select()
.from(albums) .from(albums)
.where( .where(
and(eq(albums.id, primaryAlbumId), eq(albums.ownerId, currentUser.id)) and(eq(albums.id, primaryAlbumId), inArray(albums.id, ownedAlbumIds))
) )
.limit(1); .limit(1);
@@ -51,7 +57,7 @@ export default async function handler(
const secondaryAlbums = await db const secondaryAlbums = await db
.select() .select()
.from(albums) .from(albums)
.where(and(inArray(albums.id, secondaryAlbumIds), eq(albums.ownerId, currentUser.id))); .where(and(inArray(albums.id, secondaryAlbumIds), inArray(albums.id, ownedAlbumIds)));
if (secondaryAlbums.length === 0) { if (secondaryAlbums.length === 0) {
@@ -9,7 +9,6 @@ const SELECT_ORPHAN_PHOTOS = (date: string, ownerId: string) =>
SELECT SELECT
a."id", a."id",
a."ownerId", a."ownerId",
a."deviceId",
a."type", a."type",
a."originalPath", a."originalPath",
a."isFavorite", a."isFavorite",
+3 -1
View File
@@ -3,6 +3,7 @@ import { db } from "@/config/db";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { albums } from "@/schema/albums.schema"; import { albums } from "@/schema/albums.schema";
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema"; import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
import { albumUsers } from "@/schema/albumUsers.schema";
import { eq, inArray, and } from "drizzle-orm"; import { eq, inArray, and } from "drizzle-orm";
export const config = { export const config = {
@@ -39,10 +40,11 @@ export default async function handler(
}) })
.from(albumsAssetsAssets) .from(albumsAssetsAssets)
.innerJoin(albums, eq(albumsAssetsAssets.albumId, albums.id)) .innerJoin(albums, eq(albumsAssetsAssets.albumId, albums.id))
.innerJoin(albumUsers, and(eq(albumUsers.albumId, albums.id), eq(albumUsers.role, "owner")))
.where( .where(
and( and(
inArray(albumsAssetsAssets.assetId, assetIds), inArray(albumsAssetsAssets.assetId, assetIds),
eq(albums.ownerId, currentUser.id) eq(albumUsers.userId, currentUser.id)
) )
); );
+1 -9
View File
@@ -39,19 +39,11 @@ export default async function handler(
} }
const durationExpr = sql<number>` const durationExpr = sql<number>`(${assets.duration}::decimal / 1000)`;
(
CAST(SUBSTRING(${assets.duration}, 1, 2) AS DECIMAL) * 3600 +
CAST(SUBSTRING(${assets.duration}, 4, 2) AS DECIMAL) * 60 +
CAST(SUBSTRING(${assets.duration}, 7, 2) AS DECIMAL) +
CAST(SUBSTRING(${assets.duration}, 10, 3) AS DECIMAL) / 1000
)
`;
const dbAssets = await db const dbAssets = await db
.selectDistinctOn([assets.id], { .selectDistinctOn([assets.id], {
id: assets.id, id: assets.id,
deviceId: assets.deviceId,
type: assets.type, type: assets.type,
originalPath: assets.originalPath, originalPath: assets.originalPath,
isFavorite: assets.isFavorite, isFavorite: assets.isFavorite,
@@ -15,7 +15,6 @@ const getRowsByDates = async (startDateDate: Date, endDateDate: Date, currentUse
return db return db
.select({ .select({
id: assets.id, id: assets.id,
deviceId: assets.deviceId,
type: assets.type, type: assets.type,
originalPath: assets.originalPath, originalPath: assets.originalPath,
isFavorite: assets.isFavorite, isFavorite: assets.isFavorite,
@@ -46,7 +45,6 @@ const getRowsByDates = async (startDateDate: Date, endDateDate: Date, currentUse
const getRowsByAlbums = async (currentUser: IUser, albumId: string) => { const getRowsByAlbums = async (currentUser: IUser, albumId: string) => {
return db.select({ return db.select({
id: assets.id, id: assets.id,
deviceId: assets.deviceId,
type: assets.type, type: assets.type,
originalPath: assets.originalPath, originalPath: assets.originalPath,
isFavorite: assets.isFavorite, isFavorite: assets.isFavorite,
-1
View File
@@ -67,7 +67,6 @@ export default async function handler(
const dbAssets = await db const dbAssets = await db
.selectDistinctOn([assets.id], { .selectDistinctOn([assets.id], {
id: assets.id, id: assets.id,
deviceId: assets.deviceId,
type: assets.type, type: assets.type,
originalPath: assets.originalPath, originalPath: assets.originalPath,
isFavorite: assets.isFavorite, isFavorite: assets.isFavorite,
+1 -2
View File
@@ -56,7 +56,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const dbAssets = await db.select({ const dbAssets = await db.select({
id: assets.id, id: assets.id,
deviceId: assets.deviceId,
type: assets.type, type: assets.type,
originalPath: assets.originalPath, originalPath: assets.originalPath,
isFavorite: assets.isFavorite, isFavorite: assets.isFavorite,
@@ -92,7 +91,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return { return {
...asset, ...asset,
localDateTime: new Date(asset.localDateTime), localDateTime: new Date(asset.localDateTime),
duration: asset.duration ? parseInt(asset.duration) : 0, duration: asset.duration ?? 0,
exifImageWidth: (isFlipped(asset.orientation) ? asset.exifImageHeight : asset.exifImageWidth) ?? 0, exifImageWidth: (isFlipped(asset.orientation) ? asset.exifImageHeight : asset.exifImageWidth) ?? 0,
exifImageHeight: (isFlipped(asset.orientation) ? asset.exifImageWidth : asset.exifImageHeight) ?? 0, exifImageHeight: (isFlipped(asset.orientation) ? asset.exifImageWidth : asset.exifImageHeight) ?? 0,
dateTimeOriginal: new Date(asset.dateTimeOriginal || new Date()).toISOString(), dateTimeOriginal: new Date(asset.dateTimeOriginal || new Date()).toISOString(),
+4 -2
View File
@@ -19,10 +19,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Test asset upload permission by calling a no-op endpoint // Test asset upload permission by calling a no-op endpoint
try { try {
const response = await fetch(`${ENV.IMMICH_URL}/api/assets/exist`, { const response = await fetch(`${ENV.IMMICH_URL}/api/assets/bulk-upload-check`, {
method: "POST", method: "POST",
headers, headers,
body: JSON.stringify({ deviceAssetIds: ["permission-check"], deviceId: "immich-power-tools-check" }), body: JSON.stringify({
assets: [{ id: "permission-check", checksum: "0000000000000000000000000000000000000000" }],
}),
}); });
result.canUpload = response.ok; result.canUpload = response.ok;
} catch { } catch {
+8
View File
@@ -0,0 +1,8 @@
import { pgTable, uuid, varchar, timestamp } from 'drizzle-orm/pg-core';
export const albumUsers = pgTable('album_user', {
albumId: uuid('albumId').notNull(),
userId: uuid('userId').notNull(),
role: varchar('role').notNull().default('editor'),
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(),
});
-1
View File
@@ -2,7 +2,6 @@ import { pgTable, uuid, varchar, timestamp, boolean, text } from 'drizzle-orm/pg
export const albums = pgTable('album', { export const albums = pgTable('album', {
id: uuid('id').defaultRandom().primaryKey().notNull(), id: uuid('id').defaultRandom().primaryKey().notNull(),
ownerId: uuid('ownerId').notNull(),
albumName: varchar('albumName').notNull().default('Untitled Album'), albumName: varchar('albumName').notNull().default('Untitled Album'),
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(), createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(),
albumThumbnailAssetId: uuid('albumThumbnailAssetId'), albumThumbnailAssetId: uuid('albumThumbnailAssetId'),
+2 -4
View File
@@ -1,16 +1,14 @@
import { pgTable, uuid, varchar, timestamp, boolean } from 'drizzle-orm/pg-core'; import { pgTable, uuid, varchar, timestamp, boolean, integer } from 'drizzle-orm/pg-core';
export const assets = pgTable('asset', { export const assets = pgTable('asset', {
id: uuid('id').defaultRandom().primaryKey().notNull(), id: uuid('id').defaultRandom().primaryKey().notNull(),
deviceAssetId: varchar('deviceAssetId').notNull(),
ownerId: uuid('ownerId').notNull(), ownerId: uuid('ownerId').notNull(),
deviceId: varchar('deviceId').notNull(),
type: varchar('type').notNull(), type: varchar('type').notNull(),
originalPath: varchar('originalPath').notNull(), originalPath: varchar('originalPath').notNull(),
fileCreatedAt: timestamp('fileCreatedAt', { withTimezone: true }).notNull(), fileCreatedAt: timestamp('fileCreatedAt', { withTimezone: true }).notNull(),
fileModifiedAt: timestamp('fileModifiedAt', { withTimezone: true }).notNull(), fileModifiedAt: timestamp('fileModifiedAt', { withTimezone: true }).notNull(),
isFavorite: boolean('isFavorite').default(false).notNull(), isFavorite: boolean('isFavorite').default(false).notNull(),
duration: varchar('duration'), duration: integer('duration'),
// checksum: bytea('checksum').notNull(), // checksum: bytea('checksum').notNull(),
visibility: varchar('visibility').default('timeline').notNull(), visibility: varchar('visibility').default('timeline').notNull(),
livePhotoVideoId: uuid('livePhotoVideoId').references((): any => assets.id, { onDelete: 'set null', onUpdate: 'cascade' }), livePhotoVideoId: uuid('livePhotoVideoId').references((): any => assets.id, { onDelete: 'set null', onUpdate: 'cascade' }),
+1 -6
View File
@@ -1,7 +1,6 @@
export interface IAsset { export interface IAsset {
id: string; id: string;
ownerId: string; ownerId: string;
deviceId: string;
type: string; type: string;
originalPath: string; originalPath: string;
isFavorite: boolean; isFavorite: boolean;
@@ -32,9 +31,7 @@ export interface IDuplicateAssetRecord {
export interface IDuplicateAsset { export interface IDuplicateAsset {
id: string; id: string;
deviceAssetId: string;
ownerId: string; ownerId: string;
deviceId: string;
libraryId: null; libraryId: null;
type: string; type: string;
originalPath: string; originalPath: string;
@@ -46,10 +43,8 @@ export interface IDuplicateAsset {
localDateTime: Date; localDateTime: Date;
updatedAt: Date; updatedAt: Date;
isFavorite: boolean; isFavorite: boolean;
isArchived: boolean;
isTrashed: boolean;
visibility: string; visibility: string;
duration: string; duration: number | null;
exifInfo: IDuplicateAssetExifInfo; exifInfo: IDuplicateAssetExifInfo;
livePhotoVideoId: null; livePhotoVideoId: null;
people: any[]; people: any[];