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:
1 parent
303abc72cd
commit
af4cfceb47
20 files changed
+109
-78
No files matched your search
@@ -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>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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}`}>
|
||||||
|
|||||||
@@ -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`;
|
|
||||||
}
|
}
|
||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,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)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
});
|
||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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' }),
|
||||||
|
|||||||
Vendored
+1
-6
@@ -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[];
|
||||||
|
|||||||
Reference in new issue
Block a user